From 4830e6f84818216896dc93ff7786d726ea6577bc Mon Sep 17 00:00:00 2001 From: Facu Spagnuolo Date: Tue, 20 Jun 2023 22:52:05 -0300 Subject: [PATCH 1/7] price oracle: merge on and off chain oracles --- packages/deployer/contracts/Deployer.sol | 68 ++- packages/deployer/test/Deployer.test.ts | 147 +++++- .../contracts/utils/BytesHelpers.sol} | 20 +- .../price-oracle/contracts/PriceOracle.sol | 259 ++++++++-- .../contracts/interfaces/IPriceOracle.sol | 77 ++- .../contracts/test/PriceFeedProviderMock.sol | 19 - .../price-oracle/test/PriceOracle.mainnet.ts | 71 +-- .../price-oracle/test/PriceOracle.polygon.ts | 72 +-- .../price-oracle/test/PriceOracle.test.ts | 466 ++++++++++++++++-- packages/smart-vault/contracts/SmartVault.sol | 92 +--- .../contracts/interfaces/ISmartVault.sol | 48 +- packages/smart-vault/test/SmartVaults.test.ts | 184 ++----- packages/tasks/contracts/BaseTask.sol | 12 +- packages/tasks/test/BaseTask.test.ts | 18 +- 14 files changed, 1086 insertions(+), 467 deletions(-) rename packages/{price-oracle/contracts/interfaces/IPriceFeedProvider.sol => helpers/contracts/utils/BytesHelpers.sol} (59%) delete mode 100644 packages/price-oracle/contracts/test/PriceFeedProviderMock.sol diff --git a/packages/deployer/contracts/Deployer.sol b/packages/deployer/contracts/Deployer.sol index 88682d62..7ac2eef5 100644 --- a/packages/deployer/contracts/Deployer.sol +++ b/packages/deployer/contracts/Deployer.sol @@ -18,6 +18,7 @@ import 'solmate/src/utils/CREATE3.sol'; import '@openzeppelin/contracts/utils/Address.sol'; import '@mimic-fi/v3-authorizer/contracts/Authorizer.sol'; +import '@mimic-fi/v3-price-oracle/contracts/PriceOracle.sol'; import '@mimic-fi/v3-smart-vault/contracts/SmartVault.sol'; import '@mimic-fi/v3-registry/contracts/interfaces/IRegistry.sol'; @@ -28,10 +29,15 @@ contract Deployer { IRegistry public immutable registry; /** - * @dev Emitted every time a permissions manager is deployed + * @dev Emitted every time an authorizer is deployed */ event AuthorizerDeployed(string namespace, string name, address instance, address implementation); + /** + * @dev Emitted every time a price oracle is deployed + */ + event PriceOracleDeployed(string namespace, string name, address instance, address implementation); + /**Bas * @dev Emitted every time a smart vault is deployed */ @@ -60,18 +66,32 @@ contract Deployer { address[] owners; } + /** + * @dev Price oracle params + * @param impl Address of the Price Oracle implementation to be used + * @param authorizer Address of the authorizer to be linked + * @param signer Address of the allowed signer + * @param pivot Address of the token to be used as the pivot + * @param feeds List of feeds to be set for the price oracle + */ + struct PriceOracleParams { + address impl; + address authorizer; + address signer; + address pivot; + PriceOracle.FeedData[] feeds; + } + /** * @dev Smart vault params * @param impl Address of the Smart Vault implementation to be used - * @param authorized Address of the authorizer to be linked - * @param priceOracle Optional Price Oracle to set for the Smart Vault - * @param priceFeedParams List of price feeds to be set for the Smart Vault + * @param authorizer Address of the authorizer to be linked + * @param priceOracle Optional price Oracle to set for the Smart Vault */ struct SmartVaultParams { address impl; address authorizer; address priceOracle; - SmartVault.PriceFeed[] priceFeedParams; } /** @@ -104,17 +124,29 @@ contract Deployer { * @dev Deploys a new authorizer instance */ function deployAuthorizer(string memory namespace, string memory name, AuthorizerParams memory params) external { - address instance = _deployClone(namespace, name, params.impl, true); + _validateImplementation(params.impl); + address instance = _deployClone(namespace, name, params.impl); Authorizer(instance).initialize(params.owners); emit AuthorizerDeployed(namespace, name, instance, params.impl); } + /** + * @dev Deploys a new price oracle instance + */ + function deployPriceOracle(string memory namespace, string memory name, PriceOracleParams memory params) external { + _validateImplementation(params.impl); + address instance = _deployClone(namespace, name, params.impl); + PriceOracle(instance).initialize(params.authorizer, params.signer, params.pivot, params.feeds); + emit PriceOracleDeployed(namespace, name, instance, params.impl); + } + /** * @dev Deploys a new smart vault instance */ function deploySmartVault(string memory namespace, string memory name, SmartVaultParams memory params) external { - address payable instance = payable(_deployClone(namespace, name, params.impl, true)); - SmartVault(instance).initialize(params.authorizer, params.priceOracle, params.priceFeedParams); + _validateImplementation(params.impl); + address payable instance = payable(_deployClone(namespace, name, params.impl)); + SmartVault(instance).initialize(params.authorizer, params.priceOracle); emit SmartVaultDeployed(namespace, name, instance, params.impl); } @@ -122,23 +154,29 @@ contract Deployer { * @dev Deploys a new task instance */ function deployTask(string memory namespace, string memory name, TaskParams memory params) external { - address instance = _deployClone(namespace, name, params.impl, !params.custom); + if (!params.custom) _validateImplementation(params.impl); + address instance = _deployClone(namespace, name, params.impl); if (params.initializeData.length > 0) instance.functionCall(params.initializeData, 'DEPLOYER_TASK_INIT_FAILED'); emit TaskDeployed(namespace, name, instance, params.impl); } + /** + * @dev Validates if an implementation is registered, not deprecated, and considered stateful + * @param implementation Address of the implementation to be checked + */ + function _validateImplementation(address implementation) internal view { + require(registry.isRegistered(implementation), 'DEPLOYER_IMPL_NOT_REGISTERED'); + require(!registry.isStateless(implementation), 'DEPLOYER_IMPL_STATELESS'); + require(!registry.isDeprecated(implementation), 'DEPLOYER_IMPL_DEPRECATED'); + } + /** * @dev Deploys a new clone using CREATE3 */ - function _deployClone(string memory namespace, string memory name, address implementation, bool check) + function _deployClone(string memory namespace, string memory name, address implementation) internal returns (address) { - if (check) { - require(registry.isRegistered(implementation), 'DEPLOYER_IMPL_NOT_REGISTERED'); - require(!registry.isDeprecated(implementation), 'DEPLOYER_IMPL_DEPRECATED'); - } - bytes memory bytecode = abi.encodePacked( hex'3d602d80600a3d3981f3363d3d373d3d3d363d73', implementation, diff --git a/packages/deployer/test/Deployer.test.ts b/packages/deployer/test/Deployer.test.ts index 90bb8788..f59af67f 100644 --- a/packages/deployer/test/Deployer.test.ts +++ b/packages/deployer/test/Deployer.test.ts @@ -6,6 +6,7 @@ import { Contract } from 'ethers' const ARTIFACTS = { REGISTRY: '@mimic-fi/v3-registry/artifacts/contracts/Registry.sol/Registry', AUTHORIZER: '@mimic-fi/v3-authorizer/artifacts/contracts/Authorizer.sol/Authorizer', + PRICE_ORACLE: '@mimic-fi/v3-price-oracle/artifacts/contracts/PriceOracle.sol/PriceOracle', SMART_VAULT: '@mimic-fi/v3-smart-vault/artifacts/contracts/SmartVault.sol/SmartVault', } @@ -138,14 +139,12 @@ describe('Deployer', () => { }) }) - describe('deploySmartVault', () => { - let smartVault: Contract - - const FEE_CONTROLLER = '0x0000000000000000000000000000000000000001' - const WRAPPED_NATIVE_TOKEN = '0x0000000000000000000000000000000000000002' + describe('deployPriceOracle', () => { + let priceOracle: Contract + const PIVOT = '0x0000000000000000000000000000000000000001' + const SIGNER = '0x0000000000000000000000000000000000000002' const AUTHORIZER = '0x0000000000000000000000000000000000000003' - const PRICE_ORACLE = '0x0000000000000000000000000000000000000004' const BASE_1 = '0x000000000000000000000000000000000000000a' const BASE_2 = '0x000000000000000000000000000000000000000b' @@ -154,15 +153,143 @@ describe('Deployer', () => { const FEED_1 = '0x000000000000000000000000000000000000000E' const FEED_2 = '0x000000000000000000000000000000000000000F' - const smartVaultParams = { + const priceOracleParams = { authorizer: AUTHORIZER, - priceOracle: PRICE_ORACLE, - priceFeedParams: [ + pivot: PIVOT, + signer: SIGNER, + feeds: [ { base: BASE_1, quote: QUOTE_1, feed: FEED_1 }, { base: BASE_2, quote: QUOTE_2, feed: FEED_2 }, ], } + const namespace = 'project' + const name = 'price-oracle' + + beforeEach('create price oracle implementation', async () => { + priceOracle = await deploy(ARTIFACTS.PRICE_ORACLE) + }) + + context('when the implementation is registered', () => { + beforeEach('register implementations', async () => { + await registry.connect(mimic).register('price-oracle@0.0.1', priceOracle.address, false) + }) + + context('when the implementation is not deprecated', () => { + const itDeploysPriceOracleInstance = () => { + it('deploys the expected price oracle instance', async () => { + const tx = await deployer.deployPriceOracle(namespace, name, { + impl: priceOracle.address, + ...priceOracleParams, + }) + + const event = await assertEvent(tx, 'PriceOracleDeployed', { + namespace, + name, + implementation: priceOracle.address, + }) + + const expectedAddress = await deployer.getAddress(tx.from, namespace, name) + expect(event.args.instance).to.be.equal(expectedAddress) + }) + + it('initializes the price oracle instance correctly', async () => { + const tx = await deployer.deployPriceOracle(namespace, name, { + impl: priceOracle.address, + ...priceOracleParams, + }) + + const instance = await instanceAt( + ARTIFACTS.PRICE_ORACLE, + await deployer.getAddress(tx.from, namespace, name) + ) + + await expect(instance.initialize(AUTHORIZER, PIVOT, SIGNER, [])).to.be.revertedWith( + 'Initializable: contract is already initialized' + ) + + expect(await instance.authorizer()).to.be.equal(AUTHORIZER) + expect(await instance.pivot()).to.be.equal(PIVOT) + expect(await instance.isSignerAllowed(SIGNER)).to.be.true + + expect(await instance.getFeed(BASE_1, QUOTE_1)).to.be.equal(FEED_1) + expect(await instance.getFeed(BASE_2, QUOTE_2)).to.be.equal(FEED_2) + }) + } + + context('when the namespace and name where not used', () => { + beforeEach('set sender', () => { + deployer = deployer.connect(sender) + }) + + itDeploysPriceOracleInstance() + }) + + context('when the namespace and name where already used', () => { + beforeEach('deploy price oracle', async () => { + await deployer + .connect(sender) + .deployPriceOracle(namespace, name, { impl: priceOracle.address, ...priceOracleParams }) + }) + + context('when deploying from the same address', () => { + beforeEach('set sender', () => { + deployer = deployer.connect(sender) + }) + + it('reverts', async () => { + await expect( + deployer.deployPriceOracle(namespace, name, { impl: priceOracle.address, ...priceOracleParams }) + ).to.be.revertedWith('DEPLOYMENT_FAILED') + }) + }) + + context('when deploying from another address', () => { + beforeEach('set sender', () => { + deployer = deployer.connect(mimic) + }) + + itDeploysPriceOracleInstance() + }) + }) + }) + + context('when the implementation is deprecated', () => { + beforeEach('deprecate implementation', async () => { + await registry.connect(mimic).deprecate(priceOracle.address) + }) + + it('reverts', async () => { + await expect( + deployer.deployPriceOracle(namespace, name, { impl: priceOracle.address, ...priceOracleParams }) + ).to.be.revertedWith('DEPLOYER_IMPL_DEPRECATED') + }) + }) + }) + + context('when the implementation is not registered', () => { + it('reverts', async () => { + await expect( + deployer.deployPriceOracle(namespace, name, { impl: priceOracle.address, ...priceOracleParams }) + ).to.be.revertedWith('DEPLOYER_IMPL_NOT_REGISTERED') + }) + }) + }) + + describe('deploySmartVault', () => { + let smartVault: Contract + + const FEE_CONTROLLER = '0x0000000000000000000000000000000000000001' + const WRAPPED_NATIVE_TOKEN = '0x0000000000000000000000000000000000000002' + + const AUTHORIZER = '0x0000000000000000000000000000000000000003' + const PRICE_ORACLE = '0x0000000000000000000000000000000000000004' + + const smartVaultParams = { + authorizer: AUTHORIZER, + priceOracle: PRICE_ORACLE, + } + const namespace = 'project' const name = 'smart-vault' @@ -215,8 +342,6 @@ describe('Deployer', () => { expect(await instance.authorizer()).to.be.equal(AUTHORIZER) expect(await instance.priceOracle()).to.be.equal(PRICE_ORACLE) - expect(await instance.getPriceFeed(BASE_1, QUOTE_1)).to.be.equal(FEED_1) - expect(await instance.getPriceFeed(BASE_2, QUOTE_2)).to.be.equal(FEED_2) }) } diff --git a/packages/price-oracle/contracts/interfaces/IPriceFeedProvider.sol b/packages/helpers/contracts/utils/BytesHelpers.sol similarity index 59% rename from packages/price-oracle/contracts/interfaces/IPriceFeedProvider.sol rename to packages/helpers/contracts/utils/BytesHelpers.sol index 1d587d3e..ab9b257f 100644 --- a/packages/price-oracle/contracts/interfaces/IPriceFeedProvider.sol +++ b/packages/helpers/contracts/utils/BytesHelpers.sol @@ -12,17 +12,17 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -pragma solidity >=0.8.0; +pragma solidity ^0.8.0; /** - * @title IPriceFeedProvider - * @dev Contract providing price feed references for (base, quote) token pairs + * @title BytesHelpers + * @dev Provides a list of Bytes helper methods */ -interface IPriceFeedProvider { - /** - * @dev Tells the price feed address for (base, quote) pair. It returns the zero address if there is no one set. - * @param base Token to be rated - * @param quote Token used for the price rate - */ - function getPriceFeed(address base, address quote) external view returns (address); +library BytesHelpers { + function toUint256(bytes memory self, uint256 offset) internal pure returns (uint256 result) { + require(self.length >= offset + 32, 'BYTES_OUT_OF_BOUNDS'); + assembly { + result := mload(add(add(self, 0x20), offset)) + } + } } diff --git a/packages/price-oracle/contracts/PriceOracle.sol b/packages/price-oracle/contracts/PriceOracle.sol index 8d37ee7b..4e41e829 100644 --- a/packages/price-oracle/contracts/PriceOracle.sol +++ b/packages/price-oracle/contracts/PriceOracle.sol @@ -15,30 +15,35 @@ pragma solidity ^0.8.0; import '@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol'; + import '@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol'; +import '@openzeppelin/contracts/utils/cryptography/ECDSA.sol'; import '@openzeppelin/contracts/utils/math/SafeCast.sol'; +import '@openzeppelin/contracts/utils/structs/EnumerableSet.sol'; +import '@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol'; +import '@mimic-fi/v3-authorizer/contracts/Authorized.sol'; import '@mimic-fi/v3-helpers/contracts/math/FixedPoint.sol'; import '@mimic-fi/v3-helpers/contracts/math/UncheckedMath.sol'; +import '@mimic-fi/v3-helpers/contracts/utils/BytesHelpers.sol'; import './interfaces/IPriceOracle.sol'; -import './interfaces/IPriceFeedProvider.sol'; /** - * @title PriceOracle - * @dev Oracle that interfaces with external feeds to provide quotes for tokens based on any other token. + * @title OnChainOracle + * @dev Price oracle mixing both on-chain and off-chain oracle alternatives * - * This Price Oracle only operates with ERC20 tokens, it does not allow querying quotes for any other denomination. - * Additionally, it only supports external feeds that implement ChainLink's proposed `AggregatorV3Interface` interface. + * The on-chain oracle that interfaces with Chainlink feeds to provide rates between two tokens. This oracle only + * operates with ERC20 tokens, it does not allow querying quotes for any other denomination. Additionally, it only + * supports feeds that implement ChainLink's proposed `AggregatorV3Interface` interface. * - * IMPORTANT! As many other implementations in this repo, this contract is intended to be used as a LIBRARY, not - * a contract. Due to limitations of the Solidity compiler, it's not possible to work with immutable variables in - * libraries yet. Therefore, we are relying on contracts without storage variables so they can be safely - * delegate-called if desired. + * The off-chain oracle that uses off-chain signatures to compute prices between two tokens */ -contract PriceOracle is IPriceOracle { +contract PriceOracle is IPriceOracle, Authorized, ReentrancyGuardUpgradeable { using FixedPoint for uint256; using UncheckedMath for uint256; + using BytesHelpers for bytes; + using EnumerableSet for EnumerableSet.AddressSet; // Number of decimals used for fixed point operations: 18 uint256 private constant FP_DECIMALS = 18; @@ -47,25 +52,82 @@ contract PriceOracle is IPriceOracle { uint256 private constant INVERSE_FEED_MAX_DECIMALS = 36; // It allows denoting a single token to pivot between feeds in case a direct path is not available - address public immutable pivot; + address public pivot; + + // Mapping of feeds from "token A" to "token B" + mapping (address => mapping (address => address)) public override getFeed; + + // Enumerable set of trusted signers + EnumerableSet.AddressSet private _signers; + + /** + * @dev Feed data, only used during initialization + * @param base Token to rate + * @param quote Token used for the price rate + * @param feed Chainlink oracle to fetch the given pair price + */ + struct FeedData { + address base; + address quote; + address feed; + } /** - * @dev Creates a new Price Oracle implementation with the references that should be shared among all implementations + * @dev Price data + * @param base Token to rate + * @param quote Token used for the price rate + * @param rate Price of a token (base) expressed in `quote` + * @param deadline Expiration timestamp until when the given quote is considered valid + */ + struct PriceData { + address base; + address quote; + uint256 rate; + uint256 deadline; + } + + /** + * @dev Initializes the price oracle. + * Note this function can only be called from a function marked with the `initializer` modifier. + * @param _authorizer Address of the authorizer to be set + * @param _signer Address of the initial allowed signer * @param _pivot Address of the token to be used as the pivot + * @param _feeds List of feeds to be initialized with */ - constructor(address _pivot) { + function initialize(address _authorizer, address _signer, address _pivot, FeedData[] memory _feeds) + external + initializer + { + __ReentrancyGuard_init(); + _initialize(_authorizer); + _setSigner(_signer, true); pivot = _pivot; + for (uint256 i = 0; i < _feeds.length; i++) _setFeed(_feeds[i].base, _feeds[i].quote, _feeds[i].feed); + } + + /** + * @dev Tells whether an address is as an allowed signer or not + * @param signer Address of the signer being queried + */ + function isSignerAllowed(address signer) public view override returns (bool) { + return _signers.contains(signer); + } + + /** + * @dev Tells the list of allowed signers + */ + function getAllowedSigners() external view override returns (address[] memory) { + return _signers.values(); } /** * @dev Tells the price of a token (base) in a given quote. The response is expressed using the corresponding * number of decimals so that when performing a fixed point product of it by a `base` amount it results in * a value expressed in `quote` decimals. - * @param provider Provider to fetch the price feeds from * @param base Token to rate * @param quote Token used for the price rate */ - function getPrice(address provider, address base, address quote) external view override returns (uint256) { + function getPrice(address base, address quote) public view override returns (uint256) { if (base == quote) return FixedPoint.ONE; // If `base * result / 1e18` must be expressed in `quote` decimals, then @@ -77,39 +139,81 @@ contract PriceOracle is IPriceOracle { // No need for checked math as we are checking it manually beforehand uint256 resultDecimals = quoteDecimals.uncheckedAdd(FP_DECIMALS).uncheckedSub(baseDecimals); - (uint256 price, uint256 decimals) = _getPrice(IPriceFeedProvider(provider), base, quote); + (uint256 price, uint256 decimals) = _getPrice(base, quote); return _scalePrice(price, decimals, resultDecimals); } /** - * @dev Internal function to tell the price of a token (base) in a given quote. - * @param provider Provider to fetch the price feeds from + /** + * @dev Tries fetching a price for base/quote pair from an external encoded data. It fall-backs using the on-chain + * oracle in case the require price is missing. It reverts in case the off-chain data verification fails. + * @param base Token to rate + * @param quote Token used for the price rate + * @param data Encoded prices data along with its corresponding signature + */ + function getPrice(address base, address quote, bytes memory data) external view override returns (uint256) { + if (base == quote) return FixedPoint.ONE; + + PriceData[] memory prices = _decodePricesData(data); + for (uint256 i = 0; i < prices.length; i++) { + PriceData memory price = prices[i]; + if (price.base == base && price.quote == quote) { + require(price.deadline >= block.timestamp, 'ORACLE_PRICE_OUTDATED'); + return price.rate; + } + } + + return getPrice(base, quote); + } + + /** + * @dev Sets a signer condition + * @param signer Address of the signer to be set + * @param allowed Whether the requested signer is allowed + */ + function setSigner(address signer, bool allowed) external override nonReentrant authP(authParams(signer, allowed)) { + _setSigner(signer, allowed); + } + + /** + * @dev Sets a feed for a (base, quote) pair. Sender must be authorized. + * @param base Token base to be set + * @param quote Token quote to be set + * @param feed Feed to be set + */ + function setFeed(address base, address quote, address feed) + external + override + nonReentrant + authP(authParams(base, quote, feed)) + { + _setFeed(base, quote, feed); + } + + /** + * @dev Tells the price of a token (base) in a given quote. * @param base Token to rate * @param quote Token used for the price rate * @return price Requested price rate * @return decimals Decimals of the requested price rate */ - function _getPrice(IPriceFeedProvider provider, address base, address quote) - internal - view - returns (uint256 price, uint256 decimals) - { - address feed = provider.getPriceFeed(base, quote); + function _getPrice(address base, address quote) internal view returns (uint256 price, uint256 decimals) { + address feed = getFeed[base][quote]; if (feed != address(0)) return _getFeedData(feed); - address inverseFeed = provider.getPriceFeed(quote, base); + address inverseFeed = getFeed[quote][base]; if (inverseFeed != address(0)) return _getInversePrice(inverseFeed); - address baseFeed = provider.getPriceFeed(base, pivot); - address quoteFeed = provider.getPriceFeed(quote, pivot); + address baseFeed = getFeed[base][pivot]; + address quoteFeed = getFeed[quote][pivot]; if (baseFeed != address(0) && quoteFeed != address(0)) return _getPivotPrice(baseFeed, quoteFeed); - revert('MISSING_PRICE_FEED'); + revert('ORACLE_MISSING_FEED'); } /** - * @dev Internal function to fetch data from a price feed - * @param feed Address of the price feed to fetch data from. It must support ChainLink's `AggregatorV3Interface`. + * @dev Fetches data from a Chainlink feed + * @param feed Address of the Chainlink feed to fetch data from. It must support ChainLink `AggregatorV3Interface`. * @return price Requested price * @return decimals Decimals of the requested price */ @@ -120,8 +224,8 @@ contract PriceOracle is IPriceOracle { } /** - * @dev Internal function to report a price based on an inverse feed - * @param inverseFeed Price feed of the inverse pair + * @dev Tells a price based on an inverse feed + * @param inverseFeed Feed of the inverse pair * @return price Requested price rate * @return decimals Decimals of the requested price rate */ @@ -136,9 +240,9 @@ contract PriceOracle is IPriceOracle { } /** - * @dev Internal function to report a price based on two relative price feeds - * @param baseFeed Price feed of the base token - * @param quoteFeed Price feed of the quote token + * @dev Report a price based on two relative feeds + * @param baseFeed Feed of the base token + * @param quoteFeed Feed of the quote token * @return price Requested price rate * @return decimals Decimals of the requested price rate */ @@ -161,7 +265,7 @@ contract PriceOracle is IPriceOracle { } /** - * @dev Internal function to upscale or downscale a price rate + * @dev Upscales or downscale a price rate * @param price Value to be scaled * @param priceDecimals Decimals in which `price` is originally represented * @return resultDecimals Decimals requested for the result @@ -172,4 +276,87 @@ contract PriceOracle is IPriceOracle { ? (price * 10**(resultDecimals.uncheckedSub(priceDecimals))) : (price / 10**(priceDecimals.uncheckedSub(resultDecimals))); } + + /** + * @dev Decodes a list of off-chain encoded prices. It returns an empty array in case it is malformed. It reverts + * if the data is considered properly encoded but the signer is not allowed. + * @param data Data to be decoded + */ + function _decodePricesData(bytes memory data) internal view returns (PriceData[] memory) { + if (!_isOffChainDataEncodedProperly(data)) return new PriceData[](0); + + (PriceData[] memory prices, bytes memory signature) = abi.decode(data, (PriceData[], bytes)); + bytes32 message = ECDSA.toEthSignedMessageHash(keccak256(abi.encode(prices))); + (address recovered, ECDSA.RecoverError error) = ECDSA.tryRecover(message, signature); + require(error == ECDSA.RecoverError.NoError && isSignerAllowed(recovered), 'ORACLE_INVALID_SIGNER'); + return prices; + } + + /** + * @dev Sets the off-chain oracle signer + * @param signer Address of the signer to be set + */ + function _setSigner(address signer, bool allowed) internal { + allowed ? _signers.add(signer) : _signers.remove(signer); + emit SignerSet(signer, allowed); + } + + /** + * @dev Sets a new feed for a (base, quote) pair + * @param base Token base to be set + * @param quote Token quote to be set + * @param feed Feed to be set + */ + function _setFeed(address base, address quote, address feed) internal { + getFeed[base][quote] = feed; + emit FeedSet(base, quote, feed); + } + + /** + * @dev Tells if a data array is encoded as expected for a list off-chain prices + * @param data Data to be evaluated + */ + function _isOffChainDataEncodedProperly(bytes memory data) private pure returns (bool) { + // Check the minimum expected data length based on how ABI encoding works. + // Considering the structure (PriceData[], bytes), the encoding should have the following pattern: + // + // [ PRICES OFFSET ][ SIG OFFSET ][ PRICES DATA LENGTH ][ PRICES DATA ][ SIG LENGTH ][ VRS SIG ] + // [ 32 ][ 32 ][ 32 ][ N * 128 ][ 32 ][ 32 * 3 ] + // + // Therefore the minimum length expected is: + uint256 minimumLength = 32 + 32 + 32 + 32 + 96; + if (data.length < minimumLength) return false; + + // There must be at least the same number of bytes specified by the prices offset value: + uint256 pricesOffset = data.toUint256(0); + if (data.length < pricesOffset) return false; + + // The exact expected data length can be now computed based on the prices length: + uint256 pricesLength = data.toUint256(pricesOffset); + if (data.length != minimumLength + (pricesLength * 128)) return false; + + // The signature offset can be computed based on the prices length: + uint256 signatureOffset = data.toUint256(32); + if (signatureOffset != (32 * 3) + (pricesLength * 128)) return false; + + // Finally the signature length must be 64 or 65: + uint256 signatureLength = data.toUint256(signatureOffset); + if (signatureLength != 64 && signatureLength != 65) return false; + + // Finally confirm the data types for each of the price data attributes: + for (uint256 i = 0; i < pricesLength; i++) { + uint256 offset = i * 128; + + // Base should be a valid address + uint256 priceBase = data.toUint256(32 * 3 + offset); + if (priceBase > type(uint160).max) return false; + + // Quote should be a valid address + uint256 priceQuote = data.toUint256(32 * 4 + offset); + if (priceQuote > type(uint160).max) return false; + } + + // Otherwise the data can be decoded properly + return true; + } } diff --git a/packages/price-oracle/contracts/interfaces/IPriceOracle.sol b/packages/price-oracle/contracts/interfaces/IPriceOracle.sol index 1c3fbe3e..6dcfc379 100644 --- a/packages/price-oracle/contracts/interfaces/IPriceOracle.sol +++ b/packages/price-oracle/contracts/interfaces/IPriceOracle.sol @@ -14,22 +14,75 @@ pragma solidity >=0.8.0; +import '@mimic-fi/v3-authorizer/contracts/interfaces/IAuthorized.sol'; + /** * @title IPriceOracle - * @dev Oracle that interfaces with external feeds to provide quotes for tokens based on any other token. + * @dev Price oracle interface + * + * Tells the price of a token (base) in a given quote based the following rule: the response is expressed using the + * corresponding number of decimals so that when performing a fixed point product of it by a `base` amount it results + * in a value expressed in `quote` decimals. For example, if `base` is ETH and `quote` is USDC, then the returned + * value is expected to be expressed using 6 decimals: + * + * FixedPoint.mul(X[ETH], price[USDC/ETH]) = FixedPoint.mul(X[18], price[6]) = X * price [6] */ -interface IPriceOracle { - /** - * @dev Tells the price of a token (base) in a given quote. The response is expressed using the corresponding - * number of decimals so that when performing a fixed point product of it by a `base` amount it results in - * a value expressed in `quote` decimals. For example, if `base` is ETH and `quote` is USDC, then the returned - * value is expected to be expressed using 6 decimals: - * - * FixedPoint.mul(X[ETH], price[USDC/ETH]) = FixedPoint.mul(X[18], price[6]) = X * price [6] - * - * @param provider Contract providing the price feeds to use by the oracle +interface IPriceOracle is IAuthorized { + /** + * @dev Emitted every time a signer is changed + */ + event SignerSet(address indexed signer, bool allowed); + + /** + * @dev Emitted every time a feed is set for (base, quote) pair + */ + event FeedSet(address indexed base, address indexed quote, address feed); + + /** + * @dev Tells whether an address is as an allowed signer or not + * @param signer Address of the signer being queried + */ + function isSignerAllowed(address signer) external view returns (bool); + + /** + * @dev Tells the list of allowed signers + */ + function getAllowedSigners() external view returns (address[] memory); + + /** + * @dev Tells the price of a token `base` expressed in a token `quote` * @param base Token to rate * @param quote Token used for the price rate */ - function getPrice(address provider, address base, address quote) external view returns (uint256); + function getPrice(address base, address quote) external view returns (uint256); + + /** + * @dev Tells the price of a token `base` expressed in a token `quote` + * @param base Token to rate + * @param quote Token used for the price rate + * @param data Encoded data to validate in order to compute the requested rate + */ + function getPrice(address base, address quote, bytes memory data) external view returns (uint256); + + /** + * @dev Tells the feed address for (base, quote) pair. It returns the zero address if there is no one set. + * @param base Token to be rated + * @param quote Token used for the price rate + */ + function getFeed(address base, address quote) external view returns (address); + + /** + * @dev Sets a signer condition + * @param signer Address of the signer to be set + * @param allowed Whether the requested signer is allowed + */ + function setSigner(address signer, bool allowed) external; + + /** + * @dev Sets a feed for a (base, quote) pair + * @param base Token base to be set + * @param quote Token quote to be set + * @param feed Feed to be set + */ + function setFeed(address base, address quote, address feed) external; } diff --git a/packages/price-oracle/contracts/test/PriceFeedProviderMock.sol b/packages/price-oracle/contracts/test/PriceFeedProviderMock.sol deleted file mode 100644 index 568e3e75..00000000 --- a/packages/price-oracle/contracts/test/PriceFeedProviderMock.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import '../interfaces/IPriceFeedProvider.sol'; - -contract PriceFeedProviderMock is IPriceFeedProvider { - mapping (address => mapping (address => address)) public override getPriceFeed; - - function setPriceFeed(address base, address quote, address feed) public { - getPriceFeed[base][quote] = feed; - } - - function setPriceFeeds(address[] memory bases, address[] memory quotes, address[] memory feeds) public { - require(bases.length == quotes.length, 'SET_FEEDS_INVALID_QUOTES_LENGTH'); - require(bases.length == feeds.length, 'SET_FEEDS_INVALID_FEEDS_LENGTH'); - for (uint256 i = 0; i < bases.length; i++) setPriceFeed(bases[i], quotes[i], feeds[i]); - } -} diff --git a/packages/price-oracle/test/PriceOracle.mainnet.ts b/packages/price-oracle/test/PriceOracle.mainnet.ts index 86bfe89f..13462524 100644 --- a/packages/price-oracle/test/PriceOracle.mainnet.ts +++ b/packages/price-oracle/test/PriceOracle.mainnet.ts @@ -1,5 +1,5 @@ -import { assertAlmostEqual, deploy, fp, getSigner, impersonate, ZERO_ADDRESS } from '@mimic-fi/v3-helpers' -import { Contract } from 'ethers' +import { assertAlmostEqual, deployProxy, fp, getSigner } from '@mimic-fi/v3-helpers' +import { BigNumber, Contract } from 'ethers' /* eslint-disable no-secrets/no-secrets */ @@ -13,72 +13,79 @@ const CHAINLINK_ORACLE_USDC_ETH = '0x986b5E1e1755e3C2440e960477f25201B0a8bbD4' const CHAINLINK_ORACLE_WBTC_ETH = '0xdeb288F737066589598e9214E782fa5A8eD689e8' describe('PriceOracle', () => { - let oracle: Contract, provider: Contract + let priceOracle: Contract const ERROR = 0.01 const ETH_USD = 1610 const ETH_BTC = 0.0754 const BTC_USD = ETH_USD / ETH_BTC - before('fund deployer', async () => { - await impersonate((await getSigner()).address, fp(1000)) - }) + const getPrice = async (base: string, quote: string): Promise => { + return priceOracle['getPrice(address,address)'](base, quote) + } before('create price oracle', async () => { - oracle = await deploy('PriceOracle', [WETH, ZERO_ADDRESS]) - provider = await deploy('PriceFeedProviderMock') + const owner = await getSigner() + const authorizer = await deployProxy( + '@mimic-fi/v3-authorizer/artifacts/contracts/Authorizer.sol/Authorizer', + [], + [[owner.address]] + ) + + priceOracle = await deployProxy( + 'PriceOracle', + [], + [ + authorizer.address, + owner.address, + WETH, + [ + { base: DAI, quote: WETH, feed: CHAINLINK_ORACLE_DAI_ETH }, + { base: USDC, quote: WETH, feed: CHAINLINK_ORACLE_USDC_ETH }, + { base: WBTC, quote: WETH, feed: CHAINLINK_ORACLE_WBTC_ETH }, + ], + ] + ) }) context('WETH - DAI', () => { - before('set feed', async () => { - await provider.setPriceFeeds([DAI], [WETH], [CHAINLINK_ORACLE_DAI_ETH]) - }) - it('quotes WETH/DAI correctly', async () => { const expectedPrice = fp(ETH_USD) - const price = await oracle.getPrice(provider.address, WETH, DAI) + const price = await getPrice(WETH, DAI) assertAlmostEqual(price, expectedPrice, ERROR) }) it('quotes DAI/WETH correctly', async () => { const expectedPrice = fp(1 / ETH_USD) - const price = await oracle.getPrice(provider.address, DAI, WETH) + const price = await getPrice(DAI, WETH) assertAlmostEqual(price, expectedPrice, ERROR) }) }) context('WETH - USDC', () => { - before('set feed', async () => { - await provider.setPriceFeeds([USDC], [WETH], [CHAINLINK_ORACLE_USDC_ETH]) - }) - it('quotes WETH/USDC correctly', async () => { const expectedPrice = fp(ETH_USD).div(1e12) // 6 decimals => WETH * price / 1e18 = USDC - const price = await oracle.getPrice(provider.address, WETH, USDC) + const price = await getPrice(WETH, USDC) assertAlmostEqual(price, expectedPrice, ERROR) }) it('quotes USDC/WETH correctly', async () => { const expectedPrice = fp(1 / ETH_USD).mul(1e12) // 30 decimals => USDC * price / 1e18 = WETH - const price = await oracle.getPrice(provider.address, USDC, WETH) + const price = await getPrice(USDC, WETH) assertAlmostEqual(price, expectedPrice, ERROR) }) }) context('WETH - WBTC', () => { - before('set feed', async () => { - await provider.setPriceFeeds([WBTC], [WETH], [CHAINLINK_ORACLE_WBTC_ETH]) - }) - it('quotes WETH/WBTC correctly', async () => { const expectedPrice = fp(ETH_BTC).div(1e10) // 8 decimals => WETH * price / 1e18 = WBTC - const price = await oracle.getPrice(provider.address, WETH, WBTC) + const price = await getPrice(WETH, WBTC) assertAlmostEqual(price, expectedPrice, ERROR) }) it('quotes WBTC/WETH correctly', async () => { const expectedPrice = fp(1 / ETH_BTC).mul(1e10) // 28 decimals => WBTC * price / 1e18 = WETH - const price = await oracle.getPrice(provider.address, WBTC, WETH) + const price = await getPrice(WBTC, WETH) assertAlmostEqual(price, expectedPrice, ERROR) }) }) @@ -86,13 +93,13 @@ describe('PriceOracle', () => { context('WBTC - USDC', () => { it('quotes WBTC/USDC correctly', async () => { const expectedPrice = fp(BTC_USD).div(1e2) // 16 decimals => WBTC * price / 1e18 = USDC - const price = await oracle.getPrice(provider.address, WBTC, USDC) + const price = await getPrice(WBTC, USDC) assertAlmostEqual(price, expectedPrice, ERROR) }) it('quotes USDC/WBTC correctly', async () => { const expectedPrice = fp(1 / BTC_USD).mul(1e2) // 20 decimals => USDC * price / 1e18 = WBTC - const price = await oracle.getPrice(provider.address, USDC, WBTC) + const price = await getPrice(USDC, WBTC) assertAlmostEqual(price, expectedPrice, ERROR) }) }) @@ -100,13 +107,13 @@ describe('PriceOracle', () => { context('WBTC - DAI', () => { it('quotes WBTC/DAI correctly', async () => { const expectedPrice = fp(BTC_USD).mul(1e10) // 28 decimals => WBTC * price / 1e18 = DAI - const price = await oracle.getPrice(provider.address, WBTC, DAI) + const price = await getPrice(WBTC, DAI) assertAlmostEqual(price, expectedPrice, ERROR) }) it('quotes DAI/WBTC correctly', async () => { const expectedPrice = fp(1 / BTC_USD).div(1e10) // 8 decimals => DAI * price / 1e18 = WBTC - const price = await oracle.getPrice(provider.address, DAI, WBTC) + const price = await getPrice(DAI, WBTC) assertAlmostEqual(price, expectedPrice, ERROR) }) }) @@ -114,13 +121,13 @@ describe('PriceOracle', () => { context('DAI - USDC', () => { it('quotes DAI/USDC correctly', async () => { const expectedPrice = fp(1).div(1e12) // 6 decimals => DAI * price / 1e18 = USDC - const price = await oracle.getPrice(provider.address, DAI, USDC) + const price = await getPrice(DAI, USDC) assertAlmostEqual(price, expectedPrice, ERROR) }) it('quotes USDC/DAI correctly', async () => { const expectedPrice = fp(1).mul(1e12) // 30 decimals => USDC * price / 1e18 = DAI - const price = await oracle.getPrice(provider.address, USDC, DAI) + const price = await getPrice(USDC, DAI) assertAlmostEqual(price, expectedPrice, ERROR) }) }) diff --git a/packages/price-oracle/test/PriceOracle.polygon.ts b/packages/price-oracle/test/PriceOracle.polygon.ts index be264032..92d8d504 100644 --- a/packages/price-oracle/test/PriceOracle.polygon.ts +++ b/packages/price-oracle/test/PriceOracle.polygon.ts @@ -1,5 +1,5 @@ -import { assertAlmostEqual, deploy, fp, getSigner, impersonate, ZERO_ADDRESS } from '@mimic-fi/v3-helpers' -import { Contract } from 'ethers' +import { assertAlmostEqual, deployProxy, fp, getSigner } from '@mimic-fi/v3-helpers' +import { BigNumber, Contract } from 'ethers' /* eslint-disable no-secrets/no-secrets */ @@ -15,72 +15,80 @@ const CHAINLINK_ORACLE_WETH_USD = '0xF9680D99D6C9589e2a93a78A04A279e509205945' const CHAINLINK_ORACLE_WBTC_USD = '0xc907E116054Ad103354f2D350FD2514433D57F6f' describe('PriceOracle', () => { - let oracle: Contract, provider: Contract + let priceOracle: Contract const ERROR = 0.01 const ETH_USD = 1518 const BTC_USD = 20720 const ETH_BTC = ETH_USD / BTC_USD - before('fund deployer', async () => { - await impersonate((await getSigner()).address, fp(1000)) - }) + const getPrice = async (base: string, quote: string): Promise => { + return priceOracle['getPrice(address,address)'](base, quote) + } before('create price oracle', async () => { - oracle = await deploy('PriceOracle', [USD, ZERO_ADDRESS]) - provider = await deploy('PriceFeedProviderMock') + const owner = await getSigner() + const authorizer = await deployProxy( + '@mimic-fi/v3-authorizer/artifacts/contracts/Authorizer.sol/Authorizer', + [], + [[owner.address]] + ) + + priceOracle = await deployProxy( + 'PriceOracle', + [], + [ + authorizer.address, + owner.address, + USD, + [ + { base: DAI, quote: USD, feed: CHAINLINK_ORACLE_DAI_USD }, + { base: USDC, quote: USD, feed: CHAINLINK_ORACLE_USDC_USD }, + { base: WETH, quote: USD, feed: CHAINLINK_ORACLE_WETH_USD }, + { base: WBTC, quote: USD, feed: CHAINLINK_ORACLE_WBTC_USD }, + ], + ] + ) }) context('WETH - DAI', () => { - before('set feed', async () => { - await provider.setPriceFeeds([DAI, WETH], [USD, USD], [CHAINLINK_ORACLE_DAI_USD, CHAINLINK_ORACLE_WETH_USD]) - }) - it('quotes WETH/DAI correctly', async () => { const expectedPrice = fp(ETH_USD) - const price = await oracle.getPrice(provider.address, WETH, DAI) + const price = await getPrice(WETH, DAI) assertAlmostEqual(price, expectedPrice, ERROR) }) it('quotes DAI/WETH correctly', async () => { const expectedPrice = fp(1 / ETH_USD) - const price = await oracle.getPrice(provider.address, DAI, WETH) + const price = await getPrice(DAI, WETH) assertAlmostEqual(price, expectedPrice, ERROR) }) }) context('WETH - USDC', () => { - before('set feed', async () => { - await provider.setPriceFeeds([USDC, WETH], [USD, USD], [CHAINLINK_ORACLE_USDC_USD, CHAINLINK_ORACLE_WETH_USD]) - }) - it('quotes WETH/USDC correctly', async () => { const expectedPrice = fp(ETH_USD).div(1e12) // 6 decimals => WETH * price / 1e18 = USDC - const price = await oracle.getPrice(provider.address, WETH, USDC) + const price = await getPrice(WETH, USDC) assertAlmostEqual(price, expectedPrice, ERROR) }) it('quotes USDC/WETH correctly', async () => { const expectedPrice = fp(1 / ETH_USD).mul(1e12) // 30 decimals => USDC * price / 1e18 = WETH - const price = await oracle.getPrice(provider.address, USDC, WETH) + const price = await getPrice(USDC, WETH) assertAlmostEqual(price, expectedPrice, ERROR) }) }) context('WETH - WBTC', () => { - before('set feed', async () => { - await provider.setPriceFeeds([WBTC, WETH], [USD, USD], [CHAINLINK_ORACLE_WBTC_USD, CHAINLINK_ORACLE_WETH_USD]) - }) - it('quotes WETH/WBTC correctly', async () => { const expectedPrice = fp(ETH_BTC).div(1e10) // 8 decimals => WETH * price / 1e18 = WBTC - const price = await oracle.getPrice(provider.address, WETH, WBTC) + const price = await getPrice(WETH, WBTC) assertAlmostEqual(price, expectedPrice, ERROR) }) it('quotes WBTC/WETH correctly', async () => { const expectedPrice = fp(1 / ETH_BTC).mul(1e10) // 28 decimals => WBTC * price / 1e18 = WETH - const price = await oracle.getPrice(provider.address, WBTC, WETH) + const price = await getPrice(WBTC, WETH) assertAlmostEqual(price, expectedPrice, ERROR) }) }) @@ -88,13 +96,13 @@ describe('PriceOracle', () => { context('WBTC - USDC', () => { it('quotes WBTC/USDC correctly', async () => { const expectedPrice = fp(BTC_USD).div(1e2) // 16 decimals => WBTC * price / 1e18 = USDC - const price = await oracle.getPrice(provider.address, WBTC, USDC) + const price = await getPrice(WBTC, USDC) assertAlmostEqual(price, expectedPrice, ERROR) }) it('quotes USDC/WBTC correctly', async () => { const expectedPrice = fp(1 / BTC_USD).mul(1e2) // 20 decimals => USDC * price / 1e18 = WBTC - const price = await oracle.getPrice(provider.address, USDC, WBTC) + const price = await getPrice(USDC, WBTC) assertAlmostEqual(price, expectedPrice, ERROR) }) }) @@ -102,13 +110,13 @@ describe('PriceOracle', () => { context('WBTC - DAI', () => { it('quotes WBTC/DAI correctly', async () => { const expectedPrice = fp(BTC_USD).mul(1e10) // 28 decimals => WBTC * price / 1e18 = DAI - const price = await oracle.getPrice(provider.address, WBTC, DAI) + const price = await getPrice(WBTC, DAI) assertAlmostEqual(price, expectedPrice, ERROR) }) it('quotes DAI/WBTC correctly', async () => { const expectedPrice = fp(1 / BTC_USD).div(1e10) // 8 decimals => DAI * price / 1e18 = WBTC - const price = await oracle.getPrice(provider.address, DAI, WBTC) + const price = await getPrice(DAI, WBTC) assertAlmostEqual(price, expectedPrice, ERROR) }) }) @@ -116,13 +124,13 @@ describe('PriceOracle', () => { context('DAI - USDC', () => { it('quotes DAI/USDC correctly', async () => { const expectedPrice = fp(1).div(1e12) // 6 decimals => DAI * price / 1e18 = USDC - const price = await oracle.getPrice(provider.address, DAI, USDC) + const price = await getPrice(DAI, USDC) assertAlmostEqual(price, expectedPrice, ERROR) }) it('quotes USDC/DAI correctly', async () => { const expectedPrice = fp(1).mul(1e12) // 30 decimals => USDC * price / 1e18 = DAI - const price = await oracle.getPrice(provider.address, USDC, DAI) + const price = await getPrice(USDC, DAI) assertAlmostEqual(price, expectedPrice, ERROR) }) }) diff --git a/packages/price-oracle/test/PriceOracle.test.ts b/packages/price-oracle/test/PriceOracle.test.ts index 6ac17fba..48a80106 100644 --- a/packages/price-oracle/test/PriceOracle.test.ts +++ b/packages/price-oracle/test/PriceOracle.test.ts @@ -1,23 +1,191 @@ -import { bn, deploy } from '@mimic-fi/v3-helpers' +import { + advanceTime, + assertEvent, + BigNumberish, + bn, + currentTimestamp, + DAY, + deploy, + deployProxy, + fp, + getSigner, + getSigners, + ZERO_ADDRESS, +} from '@mimic-fi/v3-helpers' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' import { expect } from 'chai' -import { Contract } from 'ethers' +import { BigNumber, Contract, ethers } from 'ethers' +import { defaultAbiCoder } from 'ethers/lib/utils' describe('PriceOracle', () => { - let oracle: Contract + let priceOracle: Contract, authorizer: Contract + let owner: SignerWithAddress, signer: SignerWithAddress const PIVOT = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE' // ETH + const BASE = '0x0000000000000000000000000000000000000001' + const QUOTE = '0x0000000000000000000000000000000000000002' + const FEED = '0x0000000000000000000000000000000000000003' - beforeEach('create oracle', async () => { - oracle = await deploy('PriceOracle', [PIVOT]) + before('setup signers', async () => { + // eslint-disable-next-line prettier/prettier + [, owner, signer] = await getSigners() }) - describe('getPrice', () => { - let provider: Contract, base: Contract, quote: Contract + beforeEach('create price oracle', async () => { + authorizer = await deployProxy( + '@mimic-fi/v3-authorizer/artifacts/contracts/Authorizer.sol/Authorizer', + [], + [[owner.address]] + ) + + priceOracle = await deployProxy( + 'PriceOracle', + [], + [authorizer.address, signer.address, PIVOT, [{ base: BASE, quote: QUOTE, feed: FEED }]] + ) + }) + + describe('initialization', async () => { + it('has an authorizer reference', async () => { + expect(await priceOracle.authorizer()).to.be.equal(authorizer.address) + }) + + it('sets the pivot properly', async () => { + expect(await priceOracle.pivot()).to.be.equal(PIVOT) + }) + + it('sets the allowed signer properly', async () => { + expect(await priceOracle.isSignerAllowed(signer.address)).to.be.true + }) + + it('sets the initial feeds properly', async () => { + expect(await priceOracle.getFeed(BASE, QUOTE)).to.be.equal(FEED) + }) + }) + + describe('setSigner', () => { + context('when the sender is authorized', () => { + beforeEach('authorize sender', async () => { + const setSignerRole = priceOracle.interface.getSighash('setSigner') + await authorizer.connect(owner).authorize(owner.address, priceOracle.address, setSignerRole, []) + priceOracle = priceOracle.connect(owner) + }) + + context('when allowing the signer', () => { + const allowed = true + + it('allows the signer', async () => { + await priceOracle.setSigner(owner.address, allowed) + expect(await priceOracle.isSignerAllowed(owner.address)).to.be.true + }) + + it('does not affect other signers', async () => { + await priceOracle.setSigner(owner.address, allowed) + expect(await priceOracle.isSignerAllowed(signer.address)).to.be.true + }) + + it('emits an event', async () => { + const tx = await priceOracle.setSigner(owner.address, allowed) + await assertEvent(tx, 'SignerSet', { signer: owner, allowed }) + }) + }) + + context('when removing the signer', () => { + const allowed = false + + it('disallows the signer', async () => { + await priceOracle.setSigner(owner.address, allowed) + expect(await priceOracle.isSignerAllowed(owner.address)).to.be.false + }) + + it('does not affect other signers', async () => { + await priceOracle.setSigner(owner.address, allowed) + expect(await priceOracle.isSignerAllowed(signer.address)).to.be.true + }) + + it('emits an event', async () => { + const tx = await priceOracle.setSigner(owner.address, allowed) + await assertEvent(tx, 'SignerSet', { signer: owner, allowed }) + }) + }) + }) + + context('when the sender is not authorized', () => { + it('reverts', async () => { + await expect(priceOracle.setSigner(owner.address, true)).to.be.revertedWith('AUTH_SENDER_NOT_ALLOWED') + }) + }) + }) + + describe('setFeed', () => { + context('when the sender is authorized', () => { + beforeEach('authorize sender', async () => { + const setFeedRole = await priceOracle.interface.getSighash('setFeed') + await authorizer.connect(owner).authorize(owner.address, priceOracle.address, setFeedRole, []) + priceOracle = priceOracle.connect(owner) + }) + + const itCanBeSet = () => { + it('can be set', async () => { + const tx = await priceOracle.setFeed(BASE, QUOTE, FEED) + + expect(await priceOracle.getFeed(BASE, QUOTE)).to.be.equal(FEED) + + await assertEvent(tx, 'FeedSet', { base: BASE, quote: QUOTE, feed: FEED }) + }) + } + + const itCanBeUnset = () => { + it('can be unset', async () => { + const tx = await priceOracle.setFeed(BASE, QUOTE, ZERO_ADDRESS) + + expect(await priceOracle.getFeed(BASE, QUOTE)).to.be.equal(ZERO_ADDRESS) + + await assertEvent(tx, 'FeedSet', { base: BASE, quote: QUOTE, feed: ZERO_ADDRESS }) + }) + } + + context('when the feed is set', () => { + beforeEach('set feed', async () => { + await priceOracle.setFeed(BASE, QUOTE, FEED) + expect(await priceOracle.getFeed(BASE, QUOTE)).to.be.equal(FEED) + }) + + itCanBeSet() + itCanBeUnset() + }) + + context('when the feed is not set', () => { + beforeEach('unset feed', async () => { + await priceOracle.setFeed(BASE, QUOTE, ZERO_ADDRESS) + expect(await priceOracle.getFeed(BASE, QUOTE)).to.be.equal(ZERO_ADDRESS) + }) + + itCanBeSet() + itCanBeUnset() + }) + }) + + context('when sender is not authorized', () => { + it('reverts', async () => { + await expect(priceOracle.setFeed(BASE, QUOTE, FEED)).to.be.revertedWith('AUTH_SENDER_NOT_ALLOWED') + }) + }) + }) + + describe('getPrice (on-chain)', () => { + let base: Contract, quote: Contract - beforeEach('deploy provider', async () => { - provider = await deploy('PriceFeedProviderMock') + beforeEach('authorize sender', async () => { + const setFeedRole = await priceOracle.interface.getSighash('setFeed') + await authorizer.connect(owner).authorize(owner.address, priceOracle.address, setFeedRole, []) + priceOracle = priceOracle.connect(owner) }) + const getPrice = async (): Promise => { + return priceOracle['getPrice(address,address)'](base.address, quote.address) + } + context('when there is no feed', () => { beforeEach('deploy tokens', async () => { base = await deploy('TokenMock', ['BASE', 18]) @@ -25,9 +193,7 @@ describe('PriceOracle', () => { }) it('reverts', async () => { - await expect(oracle.getPrice(provider.address, base.address, quote.address)).to.be.revertedWith( - 'MISSING_PRICE_FEED' - ) + await expect(getPrice()).to.be.revertedWith('ORACLE_MISSING_FEED') }) }) @@ -41,9 +207,7 @@ describe('PriceOracle', () => { }) it('reverts', async () => { - await expect(oracle.getPrice(provider.address, base.address, quote.address)).to.be.revertedWith( - 'BASE_DECIMALS_TOO_BIG' - ) + await expect(getPrice()).to.be.revertedWith('BASE_DECIMALS_TOO_BIG') }) } @@ -59,11 +223,11 @@ describe('PriceOracle', () => { beforeEach('set feed', async () => { const feed = await deploy('FeedMock', [reportedPrice, feedDecimals]) - await provider.setPriceFeeds([base.address], [quote.address], [feed.address]) + await priceOracle.setFeed(base.address, quote.address, feed.address) }) it(`expresses the price with ${resultDecimals} decimals`, async () => { - expect(await oracle.getPrice(provider.address, base.address, quote.address)).to.be.equal(expectedPrice) + expect(await getPrice()).to.be.equal(expectedPrice) }) } @@ -326,9 +490,7 @@ describe('PriceOracle', () => { }) it('reverts', async () => { - await expect(oracle.getPrice(provider.address, base.address, quote.address)).to.be.revertedWith( - 'BASE_DECIMALS_TOO_BIG' - ) + await expect(getPrice()).to.be.revertedWith('BASE_DECIMALS_TOO_BIG') }) } @@ -344,11 +506,11 @@ describe('PriceOracle', () => { beforeEach('set inverse feed', async () => { const feed = await deploy('FeedMock', [reportedInversePrice, feedDecimals]) - await provider.setPriceFeeds([quote.address], [base.address], [feed.address]) + await priceOracle.setFeed(quote.address, base.address, feed.address) }) it(`expresses the price with ${resultDecimals} decimals`, async () => { - const price = await oracle.getPrice(provider.address, base.address, quote.address) + const price = await getPrice() if (feedDecimals > 18) { // There is no precision error @@ -624,9 +786,7 @@ describe('PriceOracle', () => { }) it('reverts', async () => { - await expect(oracle.getPrice(provider.address, base.address, quote.address)).to.be.revertedWith( - 'BASE_DECIMALS_TOO_BIG' - ) + await expect(getPrice()).to.be.revertedWith('BASE_DECIMALS_TOO_BIG') }) } @@ -648,16 +808,14 @@ describe('PriceOracle', () => { beforeEach('set feed', async () => { const baseFeed = await deploy('FeedMock', [reportedBasePrice, baseFeedDecimals]) + await priceOracle.setFeed(base.address, PIVOT, baseFeed.address) + const quoteFeed = await deploy('FeedMock', [reportedQuotePrice, quoteFeedDecimals]) - await provider.setPriceFeeds( - [base.address, quote.address], - [PIVOT, PIVOT], - [baseFeed.address, quoteFeed.address] - ) + await priceOracle.setFeed(quote.address, PIVOT, quoteFeed.address) }) it(`expresses the price with ${resultDecimals} decimals`, async () => { - expect(await oracle.getPrice(provider.address, base.address, quote.address)).to.be.equal(expectedPrice) + expect(await getPrice()).to.be.equal(expectedPrice) }) } @@ -1390,4 +1548,250 @@ describe('PriceOracle', () => { }) }) }) + + describe('getPrice (off-chain)', () => { + let base: Contract, + quote: Contract, + feed: Contract, + data = '0x' + + const OFF_CHAIN_ORACLE_PRICE = fp(5) + const SMART_VAULT_ORACLE_PRICE = fp(10) + + const getPrice = async (): Promise => { + return priceOracle['getPrice(address,address,bytes)'](base.address, quote.address, data) + } + + beforeEach('deploy base and quote', async () => { + base = await deploy('TokenMock', ['BASE', 18]) + quote = await deploy('TokenMock', ['QUOTE', 18]) + feed = await deploy('FeedMock', [SMART_VAULT_ORACLE_PRICE, 18]) + }) + + const setUpSmartVaultOracleFeed = () => { + beforeEach('set smart vault oracle', async () => { + const setFeedRole = await priceOracle.interface.getSighash('setFeed') + await authorizer.connect(owner).authorize(owner.address, priceOracle.address, setFeedRole, []) + await priceOracle.connect(owner).setFeed(base.address, quote.address, feed.address) + }) + } + + const itRetrievesThePriceFromTheSmartVaultOracle = () => { + it('retrieves the pair price from the smart vault oracle', async () => { + expect(await getPrice()).to.be.equal(SMART_VAULT_ORACLE_PRICE) + }) + } + + const itRetrievesThePriceFromTheOffChainFeed = () => { + it('retrieves the pair price from the off-chain feed', async () => { + expect(await getPrice()).to.be.equal(OFF_CHAIN_ORACLE_PRICE) + }) + } + + const itRevertsDueToMissingFeed = () => { + it('reverts due to missing feed', async () => { + await expect(getPrice()).to.be.revertedWith('ORACLE_MISSING_FEED') + }) + } + + const itRevertsDueToInvalidSignature = () => { + it('reverts due to invalid signature', async () => { + await expect(getPrice()).to.be.revertedWith('ORACLE_INVALID_SIGNER') + }) + } + + context('when the feed data is well-formed', () => { + type PriceData = { base: string; quote: string; rate: BigNumberish; deadline: BigNumberish } + + let pricesData: PriceData[] + + const encodeFeedsWithSignature = async (prices: PriceData[], signer: SignerWithAddress): Promise => { + const PricesDataType = 'PriceData(address base, address quote, uint256 rate, uint256 deadline)[]' + const encodedPrices = await defaultAbiCoder.encode([PricesDataType], [prices]) + const message = ethers.utils.solidityKeccak256(['bytes'], [encodedPrices]) + const signature = await signer.signMessage(ethers.utils.arrayify(message)) + return defaultAbiCoder.encode([PricesDataType, 'bytes signature'], [prices, signature]) + } + + const itRetrievesPricesProperly = () => { + context('when the feed data is up-to-date', () => { + context('when there is no feed in the smart vault oracle', () => { + itRetrievesThePriceFromTheOffChainFeed() + }) + + context('when there is a feed in the smart vault oracle', () => { + setUpSmartVaultOracleFeed() + itRetrievesThePriceFromTheOffChainFeed() + }) + }) + + context('when the feed data is outdated', () => { + beforeEach('advance time', async () => { + await advanceTime(DAY * 2) + }) + + const itRevertsDueToOutdatedFeed = () => { + it('reverts due to outdated feed', async () => { + await expect(getPrice()).to.be.revertedWith('ORACLE_PRICE_OUTDATED') + }) + } + + context('when there is no feed in the smart vault oracle', () => { + itRevertsDueToOutdatedFeed() + }) + + context('when there is a feed in the smart vault oracle', () => { + setUpSmartVaultOracleFeed() + itRevertsDueToOutdatedFeed() + }) + }) + } + + context('when there is no off-chain feed given', () => { + beforeEach('build feed data', async () => { + pricesData = [] + }) + + context('when the feed data is properly signed', () => { + beforeEach('sign with known signer', async () => { + const signer = await getSigner(2) + data = await encodeFeedsWithSignature(pricesData, signer) + + const setSignerRole = priceOracle.interface.getSighash('setSigner') + await authorizer.connect(owner).authorize(owner.address, priceOracle.address, setSignerRole, []) + await priceOracle.connect(owner).setSigner(signer.address, true) + }) + + context('when there is no feed in the smart vault oracle', () => { + itRevertsDueToMissingFeed() + }) + + context('when there is a feed in the smart vault oracle', () => { + setUpSmartVaultOracleFeed() + itRetrievesThePriceFromTheSmartVaultOracle() + }) + }) + + context('when the feed data is not properly signed', () => { + beforeEach('sign with unknown signer', async () => { + const signer = await getSigner() + data = await encodeFeedsWithSignature(pricesData, signer) + }) + + context('when there is no feed in the smart vault oracle', () => { + itRevertsDueToInvalidSignature() + }) + + context('when there is a feed in the smart vault oracle', () => { + setUpSmartVaultOracleFeed() + itRevertsDueToInvalidSignature() + }) + }) + }) + + context('when there is only one feed given', () => { + beforeEach('build feed data', async () => { + pricesData = [ + { + base: base.address, + quote: quote.address, + rate: OFF_CHAIN_ORACLE_PRICE, + deadline: (await currentTimestamp()).add(DAY), + }, + ] + }) + + context('when the feed data is properly signed', () => { + beforeEach('sign with known signer', async () => { + const signer = await getSigner() + data = await encodeFeedsWithSignature(pricesData, signer) + + const setSignerRole = priceOracle.interface.getSighash('setSigner') + await authorizer.connect(owner).authorize(owner.address, priceOracle.address, setSignerRole, []) + await priceOracle.connect(owner).setSigner(signer.address, true) + }) + + itRetrievesPricesProperly() + }) + + context('when the feed data is not properly signed', () => { + beforeEach('sign with unknown signer', async () => { + const signer = await getSigner() + data = await encodeFeedsWithSignature(pricesData, signer) + }) + + context('when there is no feed in the smart vault oracle', () => { + itRevertsDueToInvalidSignature() + }) + + context('when there is a feed in the smart vault oracle', () => { + setUpSmartVaultOracleFeed() + itRevertsDueToInvalidSignature() + }) + }) + }) + + context('when there are many feeds given', () => { + let anotherBase: Contract, anotherQuote: Contract + + before('deploy another base and quote', async () => { + anotherBase = await deploy('TokenMock', ['BASE', 18]) + anotherQuote = await deploy('TokenMock', ['QUOTE', 18]) + }) + + beforeEach('build feed data', async () => { + const deadline = (await currentTimestamp()).add(DAY) + pricesData = [ + { base: base.address, quote: anotherQuote.address, rate: OFF_CHAIN_ORACLE_PRICE.mul(2), deadline }, + { base: base.address, quote: quote.address, rate: OFF_CHAIN_ORACLE_PRICE, deadline }, + { base: anotherBase.address, quote: anotherQuote.address, rate: OFF_CHAIN_ORACLE_PRICE.mul(3), deadline }, + ] + }) + + context('when the feed data is properly signed', () => { + beforeEach('sign with known signer', async () => { + const signer = await getSigner() + data = await encodeFeedsWithSignature(pricesData, signer) + + const setSignerRole = priceOracle.interface.getSighash('setSigner') + await authorizer.connect(owner).authorize(owner.address, priceOracle.address, setSignerRole, []) + await priceOracle.connect(owner).setSigner(signer.address, true) + }) + + itRetrievesPricesProperly() + }) + + context('when the feed data is not properly signed', () => { + beforeEach('sign with unknown signer', async () => { + const signer = await getSigner() + data = await encodeFeedsWithSignature(pricesData, signer) + }) + + context('when there is no feed in the smart vault oracle', () => { + itRevertsDueToInvalidSignature() + }) + + context('when there is a feed in the smart vault oracle', () => { + setUpSmartVaultOracleFeed() + itRevertsDueToInvalidSignature() + }) + }) + }) + }) + + context('when the feed data is malformed', () => { + beforeEach('set malformed extra calldata', async () => { + data = '0xaabbccdd' + }) + + context('when there is no feed in the smart vault oracle', () => { + itRevertsDueToMissingFeed() + }) + + context('when there is a feed in the smart vault oracle', () => { + setUpSmartVaultOracleFeed() + itRetrievesThePriceFromTheSmartVaultOracle() + }) + }) + }) }) diff --git a/packages/smart-vault/contracts/SmartVault.sol b/packages/smart-vault/contracts/SmartVault.sol index bb271e8f..da003608 100644 --- a/packages/smart-vault/contracts/SmartVault.sol +++ b/packages/smart-vault/contracts/SmartVault.sol @@ -23,7 +23,6 @@ import '@mimic-fi/v3-authorizer/contracts/Authorized.sol'; import '@mimic-fi/v3-authorizer/contracts/interfaces/IAuthorizer.sol'; import '@mimic-fi/v3-fee-controller/contracts/interfaces/IFeeController.sol'; import '@mimic-fi/v3-helpers/contracts/math/FixedPoint.sol'; -import '@mimic-fi/v3-helpers/contracts/math/UncheckedMath.sol'; import '@mimic-fi/v3-helpers/contracts/utils/ERC20Helpers.sol'; import '@mimic-fi/v3-helpers/contracts/utils/IWrappedNativeToken.sol'; import '@mimic-fi/v3-price-oracle/contracts/interfaces/IPriceOracle.sol'; @@ -38,7 +37,6 @@ import './interfaces/ISmartVault.sol'; contract SmartVault is ISmartVault, Authorized, ReentrancyGuardUpgradeable { using SafeERC20 for IERC20; using FixedPoint for uint256; - using UncheckedMath for uint256; // Price oracle reference address public override priceOracle; @@ -52,20 +50,8 @@ contract SmartVault is ISmartVault, Authorized, ReentrancyGuardUpgradeable { // Wrapped native token reference address public immutable override wrappedNativeToken; - // Wrapped native token reference - mapping (address => bool) public override isDependencyCheckIgnored; - - // Mapping of price feeds from "token A" to "token B" - mapping (address => mapping (address => address)) public override getPriceFeed; - - /** - * @dev Price feed data, only used during initialization - */ - struct PriceFeed { - address base; - address quote; - address feed; - } + // Tells whether a connector check is ignored or not + mapping (address => bool) public override isConnectorCheckIgnored; /** * @dev Creates a new Smart Vault implementation with the references that should be shared among all implementations @@ -82,14 +68,12 @@ contract SmartVault is ISmartVault, Authorized, ReentrancyGuardUpgradeable { /** * @dev Initializes the Smart Vault instance * @param _authorizer Address of the authorizer to be linked - * @param _priceOracle Address of the price oracle to be set - * @param _feeds List of price feeds to be set + * @param _priceOracle Address of the price oracle to be set, it is ignored in case it's zero */ - function initialize(address _authorizer, address _priceOracle, PriceFeed[] memory _feeds) external initializer { + function initialize(address _authorizer, address _priceOracle) external initializer { __ReentrancyGuard_init(); _initialize(_authorizer); _setPriceOracle(_priceOracle); - for (uint256 i = 0; i < _feeds.length; i++) _setPriceFeed(_feeds[i].base, _feeds[i].quote, _feeds[i].feed); } /** @@ -99,15 +83,6 @@ contract SmartVault is ISmartVault, Authorized, ReentrancyGuardUpgradeable { // solhint-disable-previous-line no-empty-blocks } - /** - * @dev Tells the price of a token (base) in a given quote - * @param base Token to rate - * @param quote Token used for the price rate - */ - function getPrice(address base, address quote) external view override returns (uint256) { - return IPriceOracle(priceOracle).getPrice(address(this), base, quote); - } - /** * @dev Sets the price oracle. Sender must be authorized. * @param newPriceOracle Address of the new price oracle to be set @@ -117,32 +92,17 @@ contract SmartVault is ISmartVault, Authorized, ReentrancyGuardUpgradeable { } /** - * @dev Sets a price feed. Sender must be authorized. - * @param base Token base to be set - * @param quote Token quote to be set - * @param feed Price feed to be set + * @dev Overrides connector checks. Sender must be authorized. + * @param connector Address of the connector to override its check + * @param ignored Whether the connector check should be ignored */ - function setPriceFeed(address base, address quote, address feed) - public - override - nonReentrant - authP(authParams(base, quote, feed)) - { - _setPriceFeed(base, quote, feed); - } - - /** - * @dev Overrides dependency checks. Sender must be authorized. - * @param dependency Address of the dependency to override its check - * @param ignored Whether the dependency check should be ignored - */ - function overrideDependencyCheck(address dependency, bool ignored) + function overrideConnectorCheck(address connector, bool ignored) external nonReentrant - authP(authParams(dependency, ignored)) + authP(authParams(connector, ignored)) { - isDependencyCheckIgnored[dependency] = ignored; - emit DependencyCheckOverridden(dependency, ignored); + isConnectorCheckIgnored[connector] = ignored; + emit ConnectorCheckOverridden(connector, ignored); } /** @@ -158,7 +118,7 @@ contract SmartVault is ISmartVault, Authorized, ReentrancyGuardUpgradeable { authP(authParams(connector)) returns (bytes memory result) { - _validateDependency(connector, true); + _validateConnector(connector); result = Address.functionDelegateCall(connector, data, 'SMART_VAULT_EXECUTE_FAILED'); emit Executed(connector, data, result); } @@ -259,32 +219,18 @@ contract SmartVault is ISmartVault, Authorized, ReentrancyGuardUpgradeable { * @param newPriceOracle Address of the new price oracle to be set */ function _setPriceOracle(address newPriceOracle) internal { - require(newPriceOracle != address(0), 'SMART_VAULT_ORACLE_ZERO'); - _validateDependency(newPriceOracle, true); priceOracle = newPriceOracle; emit PriceOracleSet(newPriceOracle); } /** - * @dev Sets a price feed - * @param base Token base to be set - * @param quote Token quote to be set - * @param feed Price feed to be set - */ - function _setPriceFeed(address base, address quote, address feed) internal { - getPriceFeed[base][quote] = feed; - emit PriceFeedSet(base, quote, feed); - } - - /** - * @dev Validates a dependency against the Mimic Registry - * @param dependency Address of the dependency to validate - * @param stateless Whether the given dependency must be stateless or not + * @dev Validates a connector against the Mimic Registry + * @param connector Address of the connector to validate */ - function _validateDependency(address dependency, bool stateless) private view { - if (isDependencyCheckIgnored[dependency]) return; - require(IRegistry(registry).isRegistered(dependency), 'SMART_VAULT_DEP_NOT_REGISTERED'); - require(IRegistry(registry).isStateless(dependency) == stateless, 'SMART_VAULT_DEP_BAD_STATE_COND'); - require(!IRegistry(registry).isDeprecated(dependency), 'SMART_VAULT_DEP_DEPRECATED'); + function _validateConnector(address connector) private view { + if (isConnectorCheckIgnored[connector]) return; + require(IRegistry(registry).isRegistered(connector), 'SMART_VAULT_CON_NOT_REGISTERED'); + require(IRegistry(registry).isStateless(connector), 'SMART_VAULT_CON_NOT_STATELESS'); + require(!IRegistry(registry).isDeprecated(connector), 'SMART_VAULT_CON_DEPRECATED'); } } diff --git a/packages/smart-vault/contracts/interfaces/ISmartVault.sol b/packages/smart-vault/contracts/interfaces/ISmartVault.sol index 4864fbb8..62f309b6 100644 --- a/packages/smart-vault/contracts/interfaces/ISmartVault.sol +++ b/packages/smart-vault/contracts/interfaces/ISmartVault.sol @@ -15,26 +15,20 @@ pragma solidity >=0.8.0; import '@mimic-fi/v3-authorizer/contracts/interfaces/IAuthorized.sol'; -import '@mimic-fi/v3-price-oracle/contracts/interfaces/IPriceFeedProvider.sol'; /** * @dev Smart Vault interface */ -interface ISmartVault is IAuthorized, IPriceFeedProvider { +interface ISmartVault is IAuthorized { /** * @dev Emitted every time the price oracle is set */ event PriceOracleSet(address indexed priceOracle); /** - * @dev Emitted every time a price feed is set for (base, quote) pair + * @dev Emitted every time a connector check is overridden */ - event PriceFeedSet(address indexed base, address indexed quote, address feed); - - /** - * @dev Emitted every time a dependency check is overridden - */ - event DependencyCheckOverridden(address indexed dependency, bool ignored); + event ConnectorCheckOverridden(address indexed connector, bool ignored); /** * @dev Emitted every time `execute` is called @@ -87,24 +81,10 @@ interface ISmartVault is IAuthorized, IPriceFeedProvider { function wrappedNativeToken() external view returns (address); /** - * @dev Tells if a dependency check is ignored - * @param dependency Address of the dependency being queried - */ - function isDependencyCheckIgnored(address dependency) external view returns (bool); - - /** - * @dev Tells the price of a token (base) in a given quote - * @param base Token to rate - * @param quote Token used for the price rate + * @dev Tells if a connector check is ignored + * @param connector Address of the connector being queried */ - function getPrice(address base, address quote) external view returns (uint256); - - /** - * @dev Tells the price feed address for (base, quote) pair. It returns the zero address if there is no one set. - * @param base Token to be rated - * @param quote Token used for the price rate - */ - function getPriceFeed(address base, address quote) external view returns (address); + function isConnectorCheckIgnored(address connector) external view returns (bool); /** * @dev Sets the price oracle @@ -113,19 +93,11 @@ interface ISmartVault is IAuthorized, IPriceFeedProvider { function setPriceOracle(address newPriceOracle) external; /** - * @dev Sets a of price feed - * @param base Token base to be set - * @param quote Token quote to be set - * @param feed Price feed to be set - */ - function setPriceFeed(address base, address quote, address feed) external; - - /** - * @dev Overrides dependency checks - * @param dependency Address of the dependency to override its check - * @param ignored Whether the dependency check should be ignored + * @dev Overrides connector checks + * @param connector Address of the connector to override its check + * @param ignored Whether the connector check should be ignored */ - function overrideDependencyCheck(address dependency, bool ignored) external; + function overrideConnectorCheck(address connector, bool ignored) external; /** * @dev Executes a connector inside of the Smart Vault context diff --git a/packages/smart-vault/test/SmartVaults.test.ts b/packages/smart-vault/test/SmartVaults.test.ts index 0ce74aa2..b23cda1d 100644 --- a/packages/smart-vault/test/SmartVaults.test.ts +++ b/packages/smart-vault/test/SmartVaults.test.ts @@ -17,7 +17,7 @@ import { ethers } from 'hardhat' describe('SmartVault', () => { let smartVault: Contract - let authorizer: Contract, priceOracle: Contract, registry: Contract, feeController: Contract, wrappedNT: Contract + let authorizer: Contract, registry: Contract, feeController: Contract, wrappedNT: Contract let owner: SignerWithAddress, mimic: SignerWithAddress, feeCollector: SignerWithAddress before('setup signers', async () => { @@ -32,10 +32,6 @@ describe('SmartVault', () => { feeCollector.address, mimic.address, ]) - priceOracle = await deploy('@mimic-fi/v3-price-oracle/artifacts/contracts/PriceOracle.sol/PriceOracle', [ - wrappedNT.address, - ]) - await registry.connect(mimic).register('price-oracle@0.0.1', priceOracle.address, true) }) beforeEach('create smart vault', async () => { @@ -47,7 +43,7 @@ describe('SmartVault', () => { smartVault = await deployProxy( 'SmartVault', [registry.address, feeController.address, wrappedNT.address], - [authorizer.address, priceOracle.address, []] + [authorizer.address, ZERO_ADDRESS] ) }) @@ -68,24 +64,22 @@ describe('SmartVault', () => { expect(await smartVault.authorizer()).to.be.equal(authorizer.address) }) - it('has a price oracle reference', async () => { - expect(await smartVault.priceOracle()).to.be.equal(priceOracle.address) + it('does not have price oracle reference', async () => { + expect(await smartVault.priceOracle()).to.be.equal(ZERO_ADDRESS) }) it('cannot be initialized twice', async () => { - await expect(smartVault.initialize(authorizer.address, priceOracle.address, [])).to.be.revertedWith( + await expect(smartVault.initialize(authorizer.address, ZERO_ADDRESS)).to.be.revertedWith( 'Initializable: contract is already initialized' ) }) }) describe('setPriceOracle', () => { - let newPriceOracle: Contract + let priceOracle: Contract beforeEach('deploy implementation', async () => { - newPriceOracle = await deploy('@mimic-fi/v3-price-oracle/artifacts/contracts/PriceOracle.sol/PriceOracle', [ - wrappedNT.address, - ]) + priceOracle = await deploy('@mimic-fi/v3-price-oracle/artifacts/contracts/PriceOracle.sol/PriceOracle') }) context('when the sender is authorized', async () => { @@ -95,163 +89,61 @@ describe('SmartVault', () => { smartVault = smartVault.connect(owner) }) - const itSetsTheImplementation = () => { - it('sets the implementation', async () => { - await smartVault.setPriceOracle(newPriceOracle.address) - expect(await smartVault.priceOracle()).to.be.equal(newPriceOracle.address) - }) - - it('emits an event', async () => { - const tx = await smartVault.setPriceOracle(newPriceOracle.address) - await assertEvent(tx, 'PriceOracleSet', { priceOracle: newPriceOracle }) - }) - } - - context('when the implementation is registered', async () => { - beforeEach('deploy implementation', async () => { - await registry.connect(mimic).register('price-oracle@0.0.2', newPriceOracle.address, true) - }) - - context('when the implementation is not deprecated', async () => { - itSetsTheImplementation() - }) + it('sets the implementation', async () => { + await smartVault.setPriceOracle(priceOracle.address) - context('when the implementation is deprecated', async () => { - beforeEach('deprecate implementation', async () => { - await registry.connect(mimic).deprecate(newPriceOracle.address) - }) - - it('reverts', async () => { - await expect(smartVault.setPriceOracle(newPriceOracle.address)).to.be.revertedWith( - 'SMART_VAULT_DEP_DEPRECATED' - ) - }) - }) + expect(await smartVault.priceOracle()).to.be.equal(priceOracle.address) }) - context('when the implementation is not registered', async () => { - context('when the dependency check is not overridden', async () => { - it('reverts', async () => { - await expect(smartVault.setPriceOracle(newPriceOracle.address)).to.be.revertedWith( - 'SMART_VAULT_DEP_NOT_REGISTERED' - ) - }) - }) - - context('when the dependency check is overridden', async () => { - beforeEach('override dependency check', async () => { - const overrideRole = smartVault.interface.getSighash('overrideDependencyCheck') - await authorizer.connect(owner).authorize(owner.address, smartVault.address, overrideRole, []) - await smartVault.connect(owner).overrideDependencyCheck(newPriceOracle.address, true) - }) + it('emits an event', async () => { + const tx = await smartVault.setPriceOracle(priceOracle.address) - itSetsTheImplementation() - }) + await assertEvent(tx, 'PriceOracleSet', { priceOracle: priceOracle }) }) }) context('when the sender is not authorized', () => { it('reverts', async () => { - await expect(smartVault.setPriceOracle(newPriceOracle.address)).to.be.revertedWith('AUTH_SENDER_NOT_ALLOWED') - }) - }) - }) - - describe('setPriceFeed', () => { - const BASE = '0x0000000000000000000000000000000000000001' - const QUOTE = '0x0000000000000000000000000000000000000002' - const FEED = '0x0000000000000000000000000000000000000003' - - context('when the sender is authorized', () => { - beforeEach('authorize sender', async () => { - const setPriceFeedRole = await smartVault.interface.getSighash('setPriceFeed') - await authorizer.connect(owner).authorize(owner.address, smartVault.address, setPriceFeedRole, []) - smartVault = smartVault.connect(owner) - }) - - const itCanBeSet = () => { - it('can be set', async () => { - const tx = await smartVault.setPriceFeed(BASE, QUOTE, FEED) - - expect(await smartVault.getPriceFeed(BASE, QUOTE)).to.be.equal(FEED) - - await assertEvent(tx, 'PriceFeedSet', { base: BASE, quote: QUOTE, feed: FEED }) - }) - } - - const itCanBeUnset = () => { - it('can be unset', async () => { - const tx = await smartVault.setPriceFeed(BASE, QUOTE, ZERO_ADDRESS) - - expect(await smartVault.getPriceFeed(BASE, QUOTE)).to.be.equal(ZERO_ADDRESS) - - await assertEvent(tx, 'PriceFeedSet', { base: BASE, quote: QUOTE, feed: ZERO_ADDRESS }) - }) - } - - context('when the feed is set', () => { - beforeEach('set feed', async () => { - await smartVault.setPriceFeed(BASE, QUOTE, FEED) - expect(await smartVault.getPriceFeed(BASE, QUOTE)).to.be.equal(FEED) - }) - - itCanBeSet() - itCanBeUnset() - }) - - context('when the feed is not set', () => { - beforeEach('unset feed', async () => { - await smartVault.setPriceFeed(BASE, QUOTE, ZERO_ADDRESS) - expect(await smartVault.getPriceFeed(BASE, QUOTE)).to.be.equal(ZERO_ADDRESS) - }) - - itCanBeSet() - itCanBeUnset() - }) - }) - - context('when sender is not authorized', () => { - it('reverts', async () => { - await expect(smartVault.setPriceFeed(BASE, QUOTE, FEED)).to.be.revertedWith('AUTH_SENDER_NOT_ALLOWED') + await expect(smartVault.setPriceOracle(priceOracle.address)).to.be.revertedWith('AUTH_SENDER_NOT_ALLOWED') }) }) }) - describe('overrideDependencyCheck', () => { - let dependency: Contract + describe('overrideConnectorCheck', () => { + let connector: Contract - beforeEach('deploy dependency', async () => { - dependency = await deploy('TokenMock', ['TKN']) + beforeEach('deploy connector', async () => { + connector = await deploy('TokenMock', ['TKN']) }) it('is active by default', async () => { - expect(await smartVault.isDependencyCheckIgnored(dependency.address)).to.be.false + expect(await smartVault.isConnectorCheckIgnored(connector.address)).to.be.false }) context('when the sender is authorized', () => { beforeEach('authorize sender', async () => { - const setPriceFeedRole = await smartVault.interface.getSighash('overrideDependencyCheck') - await authorizer.connect(owner).authorize(owner.address, smartVault.address, setPriceFeedRole, []) + const overrideConnectorCheckRole = await smartVault.interface.getSighash('overrideConnectorCheck') + await authorizer.connect(owner).authorize(owner.address, smartVault.address, overrideConnectorCheckRole, []) smartVault = smartVault.connect(owner) }) const itCanBeIgnored = () => { it('can be ignored', async () => { - const tx = await smartVault.overrideDependencyCheck(dependency.address, true) + const tx = await smartVault.overrideConnectorCheck(connector.address, true) - expect(await smartVault.isDependencyCheckIgnored(dependency.address)).to.be.true + expect(await smartVault.isConnectorCheckIgnored(connector.address)).to.be.true - await assertEvent(tx, 'DependencyCheckOverridden', { dependency, ignored: true }) + await assertEvent(tx, 'ConnectorCheckOverridden', { connector, ignored: true }) }) } const itCanBeActive = () => { it('can be active', async () => { - const tx = await smartVault.overrideDependencyCheck(dependency.address, false) + const tx = await smartVault.overrideConnectorCheck(connector.address, false) - expect(await smartVault.isDependencyCheckIgnored(dependency.address)).to.be.false + expect(await smartVault.isConnectorCheckIgnored(connector.address)).to.be.false - await assertEvent(tx, 'DependencyCheckOverridden', { dependency, ignored: false }) + await assertEvent(tx, 'ConnectorCheckOverridden', { connector, ignored: false }) }) } @@ -262,8 +154,8 @@ describe('SmartVault', () => { context('when the check is ignored', () => { beforeEach('ignore check', async () => { - await smartVault.overrideDependencyCheck(dependency.address, true) - expect(await smartVault.isDependencyCheckIgnored(dependency.address)).to.be.true + await smartVault.overrideConnectorCheck(connector.address, true) + expect(await smartVault.isConnectorCheckIgnored(connector.address)).to.be.true }) itCanBeIgnored() @@ -273,7 +165,7 @@ describe('SmartVault', () => { context('when sender is not authorized', () => { it('reverts', async () => { - await expect(smartVault.overrideDependencyCheck(dependency.address, true)).to.be.revertedWith( + await expect(smartVault.overrideConnectorCheck(connector.address, true)).to.be.revertedWith( 'AUTH_SENDER_NOT_ALLOWED' ) }) @@ -341,7 +233,7 @@ describe('SmartVault', () => { }) it('reverts', async () => { - await expect(smartVault.execute(connector.address, data)).to.be.revertedWith('SMART_VAULT_DEP_DEPRECATED') + await expect(smartVault.execute(connector.address, data)).to.be.revertedWith('SMART_VAULT_CON_DEPRECATED') }) }) }) @@ -355,26 +247,26 @@ describe('SmartVault', () => { it('reverts', async () => { await expect(smartVault.execute(connector.address, data)).to.be.revertedWith( - 'SMART_VAULT_DEP_BAD_STATE_COND' + 'SMART_VAULT_CON_NOT_STATELESS' ) }) }) }) context('when the connector is not registered', async () => { - context('when the dependency check is not overridden', async () => { + context('when the connector check is not overridden', async () => { it('reverts', async () => { await expect(smartVault.execute(connector.address, data)).to.be.revertedWith( - 'SMART_VAULT_DEP_NOT_REGISTERED' + 'SMART_VAULT_CON_NOT_REGISTERED' ) }) }) - context('when the dependency check is overridden', async () => { - beforeEach('override dependency check', async () => { - const overrideRole = smartVault.interface.getSighash('overrideDependencyCheck') + context('when the connector check is overridden', async () => { + beforeEach('override connector check', async () => { + const overrideRole = smartVault.interface.getSighash('overrideConnectorCheck') await authorizer.connect(owner).authorize(owner.address, smartVault.address, overrideRole, []) - await smartVault.connect(owner).overrideDependencyCheck(connector.address, true) + await smartVault.connect(owner).overrideConnectorCheck(connector.address, true) }) itExecutesTheConnector() diff --git a/packages/tasks/contracts/BaseTask.sol b/packages/tasks/contracts/BaseTask.sol index 21e53c85..09b6cb70 100644 --- a/packages/tasks/contracts/BaseTask.sol +++ b/packages/tasks/contracts/BaseTask.sol @@ -21,6 +21,7 @@ import '@mimic-fi/v3-authorizer/contracts/Authorized.sol'; import '@mimic-fi/v3-helpers/contracts/math/FixedPoint.sol'; import '@mimic-fi/v3-helpers/contracts/utils/Denominations.sol'; import '@mimic-fi/v3-helpers/contracts/utils/ERC20Helpers.sol'; +import '@mimic-fi/v3-price-oracle/contracts/interfaces/IPriceOracle.sol'; import '@mimic-fi/v3-smart-vault/contracts/interfaces/ISmartVault.sol'; import './interfaces/IBaseTask.sol'; @@ -172,7 +173,7 @@ contract BaseTask is IBaseTask, Authorized, ReentrancyGuardUpgradeable { } /** - * @dev Internal function to transfer task's assets to the Smart Vault + * @dev Transfers task's assets to the Smart Vault * @param token Address of the token to be transferred * @param amount Amount of tokens to be transferred * @notice Denominations.NATIVE_TOKEN_ADDRESS can be used to transfer the native token balance @@ -182,11 +183,12 @@ contract BaseTask is IBaseTask, Authorized, ReentrancyGuardUpgradeable { } /** - * @dev Fetches a base/quote price from the smart vault's oracle. This function can be overwritten to implement - * a secondary way of fetching oracle prices. + * @dev Fetches a base/quote price from the smart vault's price oracle */ - function _getPrice(address base, address quote) internal view virtual returns (uint256) { - return base == quote ? FixedPoint.ONE : ISmartVault(smartVault).getPrice(base, quote); + function _getPrice(address base, address quote) internal view returns (uint256) { + address priceOracle = ISmartVault(smartVault).priceOracle(); + require(priceOracle != address(0), 'TASK_PRICE_ORACLE_NOT_SET'); + return IPriceOracle(priceOracle).getPrice(base, quote); } /** diff --git a/packages/tasks/test/BaseTask.test.ts b/packages/tasks/test/BaseTask.test.ts index ca5d7586..87c021d6 100644 --- a/packages/tasks/test/BaseTask.test.ts +++ b/packages/tasks/test/BaseTask.test.ts @@ -1,11 +1,19 @@ -import { assertEvent, deploy, deployProxy, fp, getSigners, NATIVE_TOKEN_ADDRESS } from '@mimic-fi/v3-helpers' +import { + assertEvent, + deploy, + deployProxy, + fp, + getSigners, + NATIVE_TOKEN_ADDRESS, + ZERO_ADDRESS, +} from '@mimic-fi/v3-helpers' import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' import { expect } from 'chai' import { Contract } from 'ethers' describe('BaseTask', () => { let task: Contract, smartVault: Contract - let authorizer: Contract, priceOracle: Contract, registry: Contract, feeController: Contract, wrappedNT: Contract + let authorizer: Contract, registry: Contract, feeController: Contract, wrappedNT: Contract let owner: SignerWithAddress, other: SignerWithAddress, mimic: SignerWithAddress, feeCollector: SignerWithAddress before('setup signers', async () => { @@ -20,10 +28,6 @@ describe('BaseTask', () => { feeCollector.address, mimic.address, ]) - priceOracle = await deploy('@mimic-fi/v3-price-oracle/artifacts/contracts/PriceOracle.sol/PriceOracle', [ - wrappedNT.address, - ]) - await registry.connect(mimic).register('price-oracle@0.0.1', priceOracle.address, true) }) beforeEach('create smart vault', async () => { @@ -35,7 +39,7 @@ describe('BaseTask', () => { smartVault = await deployProxy( '@mimic-fi/v3-smart-vault/artifacts/contracts/SmartVault.sol/SmartVault', [registry.address, feeController.address, wrappedNT.address], - [authorizer.address, priceOracle.address, []] + [authorizer.address, ZERO_ADDRESS, []] ) }) From 52dcd7e8b4b7640e16aa0dd575a07a40d240211b Mon Sep 17 00:00:00 2001 From: Facu Spagnuolo Date: Tue, 20 Jun 2023 23:27:03 -0300 Subject: [PATCH 2/7] chore: add hardhat config setup for integration tests --- .github/workflows/ci-price-oracle.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci-price-oracle.yml b/.github/workflows/ci-price-oracle.yml index 05ebc07a..f38677c8 100644 --- a/.github/workflows/ci-price-oracle.yml +++ b/.github/workflows/ci-price-oracle.yml @@ -43,6 +43,8 @@ jobs: uses: actions/checkout@v3 - name: Set up environment uses: ./.github/actions/setup + - name: Set up hardhat config + run: .github/scripts/setup-hardhat-config.sh ${{secrets.GOERLI_RPC}} ${{secrets.MUMBAI_RPC}} ${{secrets.MAINNET_RPC}} ${{secrets.POLYGON_RPC}} ${{secrets.OPTIMISM_RPC}} ${{secrets.ARBITRUM_RPC}} ${{secrets.GNOSIS_RPC}} ${{secrets.AVALANCHE_RPC}} ${{secrets.BSC_RPC}} ${{secrets.FANTOM_RPC}} - name: Build run: yarn build - name: Test mainnet From 86348ccf0c137f9757da6f4d623f0cf00184909c Mon Sep 17 00:00:00 2001 From: Facu Spagnuolo Date: Wed, 21 Jun 2023 00:27:19 -0300 Subject: [PATCH 3/7] connectors: implement 1inch v5 swap connector --- package.json | 5 +- packages/connectors/.prettierrc.js | 9 + packages/connectors/LICENSE | 674 ++++++++++++++++++ .../1inch/IOneInchV5AggregationRouter.sol | 49 ++ .../swap/1inch/OneInchV5Connector.sol | 68 ++ packages/connectors/hardhat.config.ts | 20 + packages/connectors/package.json | 48 ++ packages/connectors/src/1inch.ts | 47 ++ .../1inch/fixtures/1/17525323/USDC-WBTC.json | 7 + .../1inch/fixtures/1/17525323/USDC-WETH.json | 7 + .../1inch/fixtures/1/17525323/WBTC-USDC.json | 7 + .../1inch/fixtures/1/17525323/WETH-USDC.json | 7 + .../fixtures/137/44153231/USDC-WBTC.json | 7 + .../fixtures/137/44153231/USDC-WETH.json | 7 + .../fixtures/137/44153231/WBTC-USDC.json | 7 + .../fixtures/137/44153231/WETH-USDC.json | 7 + .../connectors/test/helpers/1inch/index.ts | 77 ++ .../swap/1inch/OneInchV5Connector.behavior.ts | 119 ++++ .../swap/1inch/OneInchV5Connector.mainnet.ts | 27 + .../swap/1inch/OneInchV5Connector.polygon.ts | 27 + packages/connectors/tsconfig.json | 13 + yarn.lock | 25 +- 22 files changed, 1261 insertions(+), 3 deletions(-) create mode 100644 packages/connectors/.prettierrc.js create mode 100644 packages/connectors/LICENSE create mode 100644 packages/connectors/contracts/swap/1inch/IOneInchV5AggregationRouter.sol create mode 100644 packages/connectors/contracts/swap/1inch/OneInchV5Connector.sol create mode 100644 packages/connectors/hardhat.config.ts create mode 100644 packages/connectors/package.json create mode 100644 packages/connectors/src/1inch.ts create mode 100644 packages/connectors/test/helpers/1inch/fixtures/1/17525323/USDC-WBTC.json create mode 100644 packages/connectors/test/helpers/1inch/fixtures/1/17525323/USDC-WETH.json create mode 100644 packages/connectors/test/helpers/1inch/fixtures/1/17525323/WBTC-USDC.json create mode 100644 packages/connectors/test/helpers/1inch/fixtures/1/17525323/WETH-USDC.json create mode 100644 packages/connectors/test/helpers/1inch/fixtures/137/44153231/USDC-WBTC.json create mode 100644 packages/connectors/test/helpers/1inch/fixtures/137/44153231/USDC-WETH.json create mode 100644 packages/connectors/test/helpers/1inch/fixtures/137/44153231/WBTC-USDC.json create mode 100644 packages/connectors/test/helpers/1inch/fixtures/137/44153231/WETH-USDC.json create mode 100644 packages/connectors/test/helpers/1inch/index.ts create mode 100644 packages/connectors/test/swap/1inch/OneInchV5Connector.behavior.ts create mode 100644 packages/connectors/test/swap/1inch/OneInchV5Connector.mainnet.ts create mode 100644 packages/connectors/test/swap/1inch/OneInchV5Connector.polygon.ts create mode 100644 packages/connectors/tsconfig.json diff --git a/package.json b/package.json index 758fb2ae..532f8cec 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,12 @@ "packages/helpers", "packages/registry", "packages/fee-controller", - "packages/price-oracle", "packages/authorizer", + "packages/price-oracle", "packages/smart-vault", "packages/tasks", - "packages/deployer" + "packages/deployer", + "packages/connectors" ] } } diff --git a/packages/connectors/.prettierrc.js b/packages/connectors/.prettierrc.js new file mode 100644 index 00000000..01dbc74e --- /dev/null +++ b/packages/connectors/.prettierrc.js @@ -0,0 +1,9 @@ +const ts = require('eslint-config-mimic/prettier') +const solidity = require('solhint-config-mimic/prettier') + +module.exports = { + overrides: [ + ts, + solidity + ] +} diff --git a/packages/connectors/LICENSE b/packages/connectors/LICENSE new file mode 100644 index 00000000..f288702d --- /dev/null +++ b/packages/connectors/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/packages/connectors/contracts/swap/1inch/IOneInchV5AggregationRouter.sol b/packages/connectors/contracts/swap/1inch/IOneInchV5AggregationRouter.sol new file mode 100644 index 00000000..d224fe66 --- /dev/null +++ b/packages/connectors/contracts/swap/1inch/IOneInchV5AggregationRouter.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.0; + +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; + +interface IAggregationExecutor { + /// @notice propagates information about original msg.sender and executes arbitrary data + function execute(address msgSender) external payable; +} + +interface IOneInchV5AggregationRouter { + struct SwapDescription { + IERC20 srcToken; + IERC20 dstToken; + address payable srcReceiver; + address payable dstReceiver; + uint256 amount; + uint256 minReturnAmount; + uint256 flags; + } + + /// @notice Performs a swap, delegating all calls encoded in `data` to `executor`. See tests for usage examples + /// @dev router keeps 1 wei of every token on the contract balance for gas optimisations reasons. This affects first swap of every token by leaving 1 wei on the contract. + /// @param executor Aggregation executor that executes calls described in `data` + /// @param desc Swap description + /// @param permit Should contain valid permit that can be used in `IERC20Permit.permit` calls. + /// @param data Encoded calls that `caller` should execute in between of swaps + /// @return returnAmount Resulting token amount + /// @return spentAmount Source token amount + function swap( + IAggregationExecutor executor, + SwapDescription calldata desc, + bytes calldata permit, + bytes calldata data + ) external payable returns (uint256 returnAmount, uint256 spentAmount); +} diff --git a/packages/connectors/contracts/swap/1inch/OneInchV5Connector.sol b/packages/connectors/contracts/swap/1inch/OneInchV5Connector.sol new file mode 100644 index 00000000..c8175a30 --- /dev/null +++ b/packages/connectors/contracts/swap/1inch/OneInchV5Connector.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.0; + +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import '@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol'; +import '@openzeppelin/contracts/utils/Address.sol'; + +import '@mimic-fi/v3-helpers/contracts/utils/ERC20Helpers.sol'; + +import './IOneInchV5AggregationRouter.sol'; + +/** + * @title OneInchV5Connector + * @dev Interfaces with 1inch V5 to swap tokens + */ +contract OneInchV5Connector { + // Reference to 1inch aggregation router v5 + IOneInchV5AggregationRouter public immutable oneInchV5Router; + + /** + * @dev Creates a new OneInchV5Connector contract + * @param _oneInchV5Router 1inch aggregation router v5 reference + */ + constructor(address _oneInchV5Router) { + oneInchV5Router = IOneInchV5AggregationRouter(_oneInchV5Router); + } + + /** + * @dev Executes a token swap in 1Inch V5 + * @param tokenIn Token to be sent + * @param tokenOut Token to received + * @param amountIn Amount of token in to be swapped + * @param minAmountOut Minimum amount of token out willing to receive + * @param data Calldata to be sent to the 1inch aggregation router + */ + function execute(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes memory data) + external + returns (uint256 amountOut) + { + require(tokenIn != tokenOut, '1INCH_SWAP_SAME_TOKEN'); + + uint256 preBalanceIn = IERC20(tokenIn).balanceOf(address(this)); + uint256 preBalanceOut = IERC20(tokenOut).balanceOf(address(this)); + + ERC20Helpers.approve(tokenIn, address(oneInchV5Router), amountIn); + Address.functionCall(address(oneInchV5Router), data, '1INCH_V5_SWAP_FAILED'); + + uint256 postBalanceIn = IERC20(tokenIn).balanceOf(address(this)); + require(postBalanceIn >= preBalanceIn - amountIn, '1INCH_V5_BAD_TOKEN_IN_BALANCE'); + + uint256 postBalanceOut = IERC20(tokenOut).balanceOf(address(this)); + amountOut = postBalanceOut - preBalanceOut; + require(amountOut >= minAmountOut, '1INCH_V5_MIN_AMOUNT_OUT'); + } +} diff --git a/packages/connectors/hardhat.config.ts b/packages/connectors/hardhat.config.ts new file mode 100644 index 00000000..fca1e0b0 --- /dev/null +++ b/packages/connectors/hardhat.config.ts @@ -0,0 +1,20 @@ +import '@nomiclabs/hardhat-ethers' +import '@nomiclabs/hardhat-waffle' +import '@mimic-fi/v3-helpers/dist/tests' +import 'hardhat-local-networks-config-plugin' + +import { homedir } from 'os' +import path from 'path' + +export default { + localNetworksConfig: path.join(homedir(), '/.hardhat/networks.mimic.json'), + solidity: { + version: '0.8.17', + settings: { + optimizer: { + enabled: true, + runs: 10000, + }, + }, + }, +} diff --git a/packages/connectors/package.json b/packages/connectors/package.json new file mode 100644 index 00000000..78b77707 --- /dev/null +++ b/packages/connectors/package.json @@ -0,0 +1,48 @@ +{ + "name": "@mimic-fi/v3-connectors", + "version": "0.0.1", + "license": "GPL-3.0", + "files": [ + "artifacts/contracts/**/*", + "!artifacts/contracts/test/*", + "contracts/**/*", + "!contracts/test/*" + ], + "scripts": { + "build": "yarn compile", + "compile": "hardhat compile", + "lint": "yarn lint:solidity && yarn lint:typescript", + "lint:solidity": "solhint 'contracts/**/*.sol' --config ../../node_modules/solhint-config-mimic/index.js", + "lint:typescript": "eslint . --ext .ts", + "test": "hardhat test", + "test:mainnet": "yarn test --fork mainnet --block-number 17525323", + "test:polygon": "yarn test --fork polygon --block-number 44153231", + "prepare": "yarn build" + }, + "dependencies": { + "solmate": "^6.7.0", + "@mimic-fi/v3-registry": "0.0.1", + "@openzeppelin/contracts": "4.7.0" + }, + "devDependencies": { + "@nomiclabs/hardhat-ethers": "^2.2.3", + "@nomiclabs/hardhat-waffle": "2.0.3", + "@types/chai": "^4.3.5", + "@types/mocha": "^10.0.1", + "@types/sinon-chai": "^3.2.3", + "axios": "^1.4.0", + "chai": "^4.3.7", + "eslint-config-mimic": "^0.0.2", + "ethereum-waffle": "^3.4.4", + "ethers": "~5.6.0", + "hardhat": "^2.14.1", + "hardhat-local-networks-config-plugin": "^0.0.6", + "mocha": "^10.2.0", + "solhint-config-mimic": "^0.0.2", + "ts-node": "^10.9.1", + "typescript": "~4.3.4" + }, + "eslintConfig": { + "extends": "eslint-config-mimic" + } +} diff --git a/packages/connectors/src/1inch.ts b/packages/connectors/src/1inch.ts new file mode 100644 index 00000000..60ccc537 --- /dev/null +++ b/packages/connectors/src/1inch.ts @@ -0,0 +1,47 @@ +import axios, { AxiosError } from 'axios' +import { BigNumber, Contract } from 'ethers' + +const ONE_INCH_URL = 'https://api.1inch.io/v5.0' + +export type SwapResponse = { data: { tx: { data: string } } } + +export async function get1inchSwapData( + chainId: number, + sender: Contract, + tokenIn: Contract, + tokenOut: Contract, + amountIn: BigNumber, + slippage: number +): Promise { + try { + const response = await getSwap(chainId, sender, tokenIn, tokenOut, amountIn, slippage) + return response.data.tx.data + } catch (error) { + if (error instanceof AxiosError) throw Error(error.toString() + ' - ' + error.response?.data?.description) + else throw error + } +} + +async function getSwap( + chainId: number, + sender: Contract, + tokenIn: Contract, + tokenOut: Contract, + amountIn: BigNumber, + slippage: number +): Promise { + return axios.get(`${ONE_INCH_URL}/${chainId}/swap`, { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + params: { + disableEstimate: true, + fromAddress: sender.address, + fromTokenAddress: tokenIn.address, + toTokenAddress: tokenOut.address, + amount: amountIn.toString(), + slippage: slippage < 1 ? slippage * 100 : slippage, + }, + }) +} diff --git a/packages/connectors/test/helpers/1inch/fixtures/1/17525323/USDC-WBTC.json b/packages/connectors/test/helpers/1inch/fixtures/1/17525323/USDC-WBTC.json new file mode 100644 index 00000000..6f5c3027 --- /dev/null +++ b/packages/connectors/test/helpers/1inch/fixtures/1/17525323/USDC-WBTC.json @@ -0,0 +1,7 @@ +{ + "tokenIn": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "tokenOut": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", + "amountIn": "10000000000", + "slippage": 0.015, + "data": "0xe449022e00000000000000000000000000000000000000000000000000000002540be40000000000000000000000000000000000000000000000000000000000020a42930000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000200000000000000000000000088e6a0c2ddd26feeb64f039a2c41296fcb3f56408000000000000000000000004585fe77225b41b697c938b018e2ac67ac5a20c0cfee7c08" +} \ No newline at end of file diff --git a/packages/connectors/test/helpers/1inch/fixtures/1/17525323/USDC-WETH.json b/packages/connectors/test/helpers/1inch/fixtures/1/17525323/USDC-WETH.json new file mode 100644 index 00000000..e8b8ba41 --- /dev/null +++ b/packages/connectors/test/helpers/1inch/fixtures/1/17525323/USDC-WETH.json @@ -0,0 +1,7 @@ +{ + "tokenIn": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "tokenOut": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "amountIn": "10000000000", + "slippage": 0.015, + "data": "0xe449022e00000000000000000000000000000000000000000000000000000002540be4000000000000000000000000000000000000000000000000004b88924a476a67cd0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000088e6a0c2ddd26feeb64f039a2c41296fcb3f5640cfee7c08" +} \ No newline at end of file diff --git a/packages/connectors/test/helpers/1inch/fixtures/1/17525323/WBTC-USDC.json b/packages/connectors/test/helpers/1inch/fixtures/1/17525323/WBTC-USDC.json new file mode 100644 index 00000000..6b2c0092 --- /dev/null +++ b/packages/connectors/test/helpers/1inch/fixtures/1/17525323/WBTC-USDC.json @@ -0,0 +1,7 @@ +{ + "tokenIn": "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", + "tokenOut": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "amountIn": "100000000", + "slippage": 0.015, + "data": "0xe449022e0000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000695097f210000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000099ac8ca7087fa4a2a1fb6357269965a2014abc35cfee7c08" +} \ No newline at end of file diff --git a/packages/connectors/test/helpers/1inch/fixtures/1/17525323/WETH-USDC.json b/packages/connectors/test/helpers/1inch/fixtures/1/17525323/WETH-USDC.json new file mode 100644 index 00000000..1f3d0428 --- /dev/null +++ b/packages/connectors/test/helpers/1inch/fixtures/1/17525323/WETH-USDC.json @@ -0,0 +1,7 @@ +{ + "tokenIn": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "tokenOut": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "amountIn": "1000000000000000000", + "slippage": 0.015, + "data": "0xe449022e0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000006a1d73080000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000180000000000000000000000088e6a0c2ddd26feeb64f039a2c41296fcb3f5640cfee7c08" +} \ No newline at end of file diff --git a/packages/connectors/test/helpers/1inch/fixtures/137/44153231/USDC-WBTC.json b/packages/connectors/test/helpers/1inch/fixtures/137/44153231/USDC-WBTC.json new file mode 100644 index 00000000..76fc1c76 --- /dev/null +++ b/packages/connectors/test/helpers/1inch/fixtures/137/44153231/USDC-WBTC.json @@ -0,0 +1,7 @@ +{ + "tokenIn": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + "tokenOut": "0x1bfd67037b42cf73acf2047067bd4f2c47d9bfd6", + "amountIn": "10000000000", + "slippage": 0.01, + "data": "0x12aa3caf000000000000000000000000cfd674f8731e801a4a15c1ae31770960e1afded10000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa841740000000000000000000000001bfd67037b42cf73acf2047067bd4f2c47d9bfd6000000000000000000000000cfd674f8731e801a4a15c1ae31770960e1afded1000000000000000000000000502dcaf7b2a3ce981f1a6c86cc4634b16c7a836f00000000000000000000000000000000000000000000000000000002540be40000000000000000000000000000000000000000000000000000000000020dd1670000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000034900000000000000000000000000000000000000000000000000032b0002fd00a0c9e75c48000000000000000006040000000000000000000000000000000000000000000000000002cf00016200a007e5c0d200000000000000000000000000000000000000000000000000013e00004e4820cdc878c037625afe3a98e14fcc56e169f0b5b4112791bca1f2de4661ed88a30c99a7a9449aa84174dd93f59a000000000000000000000000cfd674f8731e801a4a15c1ae31770960e1afded15120817eb46d60762442da3d931ff51a30334ca39b74c2132d05d31c914a87c6611c10748aeb04b58e8f00447dc20382000000000000000000000000c2132d05d31c914a87c6611c10748aeb04b58e8f0000000000000000000000001bfd67037b42cf73acf2047067bd4f2c47d9bfd600000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000d2531e000000000000000000000000cfd674f8731e801a4a15c1ae31770960e1afded1000000000000000000000000910bf2d50fa5e014fd06666f456182d4ab7c8bd200a0c9e75c4800000000000000002e0400000000000000000000000000000000000000000000000000013f00004f02a00000000000000000000000000000000000000000000000000000000000193d96ee63c1e500eef1a9507b3d505f0062f2be9453981255b503c82791bca1f2de4661ed88a30c99a7a9449aa841745120817eb46d60762442da3d931ff51a30334ca39b742791bca1f2de4661ed88a30c99a7a9449aa8417400447dc203820000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa841740000000000000000000000001bfd67037b42cf73acf2047067bd4f2c47d9bfd6000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000012240b2000000000000000000000000cfd674f8731e801a4a15c1ae31770960e1afded1000000000000000000000000910bf2d50fa5e014fd06666f456182d4ab7c8bd280a06c4eca271bfd67037b42cf73acf2047067bd4f2c47d9bfd61111111254eeb25477b68fb85ed929f73a9605820000000000000000000000000000000000000000000000cfee7c08" +} \ No newline at end of file diff --git a/packages/connectors/test/helpers/1inch/fixtures/137/44153231/USDC-WETH.json b/packages/connectors/test/helpers/1inch/fixtures/137/44153231/USDC-WETH.json new file mode 100644 index 00000000..14936528 --- /dev/null +++ b/packages/connectors/test/helpers/1inch/fixtures/137/44153231/USDC-WETH.json @@ -0,0 +1,7 @@ +{ + "tokenIn": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + "tokenOut": "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619", + "amountIn": "10000000000", + "slippage": 0.01, + "data": "0x12aa3caf000000000000000000000000cfd674f8731e801a4a15c1ae31770960e1afded10000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa841740000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f619000000000000000000000000cfd674f8731e801a4a15c1ae31770960e1afded1000000000000000000000000502dcaf7b2a3ce981f1a6c86cc4634b16c7a836f00000000000000000000000000000000000000000000000000000002540be4000000000000000000000000000000000000000000000000004be68b4294b17097000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b900000000000000000000000000000000000000000000000000019b00016d00a0c9e75c4800000000000000001d1500000000000000000000000000000000000000000000000000013f00004f02a00000000000000000000000000000000000000000000000001fe1020a515147a5ee63c1e50145dda9cb7c25131df268515131f647d726f506082791bca1f2de4661ed88a30c99a7a9449aa841745120817eb46d60762442da3d931ff51a30334ca39b742791bca1f2de4661ed88a30c99a7a9449aa8417400447dc203820000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa841740000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f61900000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000002c058938436028f1000000000000000000000000cfd674f8731e801a4a15c1ae31770960e1afded1000000000000000000000000910bf2d50fa5e014fd06666f456182d4ab7c8bd280a06c4eca277ceb23fd6bc0add59e62ac25578270cff1b9f6191111111254eeb25477b68fb85ed929f73a96058200000000000000cfee7c08" +} \ No newline at end of file diff --git a/packages/connectors/test/helpers/1inch/fixtures/137/44153231/WBTC-USDC.json b/packages/connectors/test/helpers/1inch/fixtures/137/44153231/WBTC-USDC.json new file mode 100644 index 00000000..945010e9 --- /dev/null +++ b/packages/connectors/test/helpers/1inch/fixtures/137/44153231/WBTC-USDC.json @@ -0,0 +1,7 @@ +{ + "tokenIn": "0x1bfd67037b42cf73acf2047067bd4f2c47d9bfd6", + "tokenOut": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + "amountIn": "100000000", + "slippage": 0.01, + "data": "0x12aa3caf000000000000000000000000cfd674f8731e801a4a15c1ae31770960e1afded10000000000000000000000001bfd67037b42cf73acf2047067bd4f2c47d9bfd60000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa84174000000000000000000000000cfd674f8731e801a4a15c1ae31770960e1afded1000000000000000000000000502dcaf7b2a3ce981f1a6c86cc4634b16c7a836f0000000000000000000000000000000000000000000000000000000005f5e100000000000000000000000000000000000000000000000000000000069c865a6a000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004b600000000000000000000000000000000000000000000000000049800046a00a0c9e75c480000000000000005030200000000000000000000000000000000000000000000043c0002800000c200a007e5c0d200000000000000000000000000000000000000000000000000009e00004f02a00000000000000000000000000000000000000000000000002b989e7177123e55ee63c1e501ac4494e30a85369e332bdb5230d6d694d4259dbc1bfd67037b42cf73acf2047067bd4f2c47d9bfd602a000000000000000000000000000000000000000000000000000000001528b4d1aee63c1e50045dda9cb7c25131df268515131f647d726f506087ceb23fd6bc0add59e62ac25578270cff1b9f61900a007e5c0d200000000000000000000000000000000000000000000000000019a0000f05120817eb46d60762442da3d931ff51a30334ca39b741bfd67037b42cf73acf2047067bd4f2c47d9bfd600447dc203820000000000000000000000001bfd67037b42cf73acf2047067bd4f2c47d9bfd6000000000000000000000000c2132d05d31c914a87c6611c10748aeb04b58e8f000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000001fbc68e67000000000000000000000000cfd674f8731e801a4a15c1ae31770960e1afded1000000000000000000000000910bf2d50fa5e014fd06666f456182d4ab7c8bd200a0c9e75c480000000000000000250d00000000000000000000000000000000000000000000000000007c00002e00a0a87a1ae8813fddeccd0401c4fa73b092b074802440544e52c2132d05d31c914a87c6611c10748aeb04b58e8f4820cdc878c037625afe3a98e14fcc56e169f0b5b411c2132d05d31c914a87c6611c10748aeb04b58e8fbd6015b4000000000000000000000000cfd674f8731e801a4a15c1ae31770960e1afded100a0c9e75c48000000000000002a040400000000000000000000000000000000000000000000018e00009e00004f02a00000000000000000000000000000000000000000000000000000000043b5da82ee63c1e501a5cd8351cbf30b531c7b11b0d9d3ff38ea2e280f1bfd67037b42cf73acf2047067bd4f2c47d9bfd602a00000000000000000000000000000000000000000000000000000000043b64c29ee63c1e501eef1a9507b3d505f0062f2be9453981255b503c81bfd67037b42cf73acf2047067bd4f2c47d9bfd65120817eb46d60762442da3d931ff51a30334ca39b741bfd67037b42cf73acf2047067bd4f2c47d9bfd600447dc203820000000000000000000000001bfd67037b42cf73acf2047067bd4f2c47d9bfd60000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa84174000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000002c6d88379000000000000000000000000cfd674f8731e801a4a15c1ae31770960e1afded1000000000000000000000000910bf2d50fa5e014fd06666f456182d4ab7c8bd280a06c4eca272791bca1f2de4661ed88a30c99a7a9449aa841741111111254eeb25477b68fb85ed929f73a96058200000000000000000000cfee7c08" +} \ No newline at end of file diff --git a/packages/connectors/test/helpers/1inch/fixtures/137/44153231/WETH-USDC.json b/packages/connectors/test/helpers/1inch/fixtures/137/44153231/WETH-USDC.json new file mode 100644 index 00000000..fb4654bf --- /dev/null +++ b/packages/connectors/test/helpers/1inch/fixtures/137/44153231/WETH-USDC.json @@ -0,0 +1,7 @@ +{ + "tokenIn": "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619", + "tokenOut": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + "amountIn": "1000000000000000000", + "slippage": 0.01, + "data": "0x12aa3caf000000000000000000000000cfd674f8731e801a4a15c1ae31770960e1afded10000000000000000000000007ceb23fd6bc0add59e62ac25578270cff1b9f6190000000000000000000000002791bca1f2de4661ed88a30c99a7a9449aa84174000000000000000000000000cfd674f8731e801a4a15c1ae31770960e1afded1000000000000000000000000502dcaf7b2a3ce981f1a6c86cc4634b16c7a836f0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000006aba46f50000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010e0000000000000000000000000000000000000000000000000000f00000c200a007e5c0d200000000000000000000000000000000000000000000000000009e00004f02a00000000000000000000000000000000000000000000000960a5972b514874f22ee63c1e500479e1b71a702a595e19b6d5932cd5c863ab57ee07ceb23fd6bc0add59e62ac25578270cff1b9f61902a0000000000000000000000000000000000000000000000000000000006aba46f5ee63c1e501a374094527e1673a86de625aa59517c5de346d320d500b1d8e8ef31e21c99d1db9a6444d3adf127080a06c4eca272791bca1f2de4661ed88a30c99a7a9449aa841741111111254eeb25477b68fb85ed929f73a960582000000000000000000000000000000000000cfee7c08" +} \ No newline at end of file diff --git a/packages/connectors/test/helpers/1inch/index.ts b/packages/connectors/test/helpers/1inch/index.ts new file mode 100644 index 00000000..5acc316a --- /dev/null +++ b/packages/connectors/test/helpers/1inch/index.ts @@ -0,0 +1,77 @@ +import { currentBlockNumber } from '@mimic-fi/v3-helpers' +import { BigNumber, Contract } from 'ethers' +import fs from 'fs' +import hre from 'hardhat' +import { HardhatNetworkConfig } from 'hardhat/types' +import path from 'path' + +import { get1inchSwapData } from '../../../src/1inch' + +type Fixture = { + tokenIn: string + tokenOut: string + amountIn: string + slippage: number + data: string +} + +export async function loadOrGet1inchSwapData( + chainId: number, + sender: Contract, + tokenIn: Contract, + tokenOut: Contract, + amountIn: BigNumber, + slippage: number +): Promise { + const config = hre.network.config as HardhatNetworkConfig + const blockNumber = config?.forking?.blockNumber?.toString() || (await currentBlockNumber()).toString() + + const fixture = await readFixture(chainId, tokenIn, tokenOut, blockNumber) + if (fixture) return fixture.data + + const data = await get1inchSwapData(chainId, sender, tokenIn, tokenOut, amountIn, slippage) + await saveFixture(chainId, tokenIn, tokenOut, amountIn, slippage, data, blockNumber) + return data +} + +async function readFixture( + chainId: number, + tokenIn: Contract, + tokenOut: Contract, + blockNumber: string +): Promise { + const swapPath = `${await tokenIn.symbol()}-${await tokenOut.symbol()}.json` + const fixturePath = path.join(__dirname, 'fixtures', chainId.toString(), blockNumber, swapPath) + if (!fs.existsSync(fixturePath)) return undefined + return JSON.parse(fs.readFileSync(fixturePath).toString()) +} + +async function saveFixture( + chainId: number, + tokenIn: Contract, + tokenOut: Contract, + amountIn: BigNumber, + slippage: number, + data: string, + blockNumber: string +): Promise { + const output = { + tokenIn: tokenIn.address, + tokenOut: tokenOut.address, + amountIn: amountIn.toString(), + slippage, + data, + } + + const fixturesPath = path.join(__dirname, 'fixtures') + if (!fs.existsSync(fixturesPath)) fs.mkdirSync(fixturesPath) + + const networkPath = path.join(fixturesPath, chainId.toString()) + if (!fs.existsSync(networkPath)) fs.mkdirSync(networkPath) + + const blockNumberPath = path.join(networkPath, blockNumber) + if (!fs.existsSync(blockNumberPath)) fs.mkdirSync(blockNumberPath) + + const swapPath = path.join(blockNumberPath, `${await tokenIn.symbol()}-${await tokenOut.symbol()}.json`) + fs.writeFileSync(swapPath, JSON.stringify(output, null, 2)) +} diff --git a/packages/connectors/test/swap/1inch/OneInchV5Connector.behavior.ts b/packages/connectors/test/swap/1inch/OneInchV5Connector.behavior.ts new file mode 100644 index 00000000..172ca27a --- /dev/null +++ b/packages/connectors/test/swap/1inch/OneInchV5Connector.behavior.ts @@ -0,0 +1,119 @@ +import { deployProxy, fp, impersonate, instanceAt, pct, toUSDC, toWBTC, ZERO_ADDRESS } from '@mimic-fi/v3-helpers' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' +import { expect } from 'chai' +import { BigNumber, Contract } from 'ethers' + +import { loadOrGet1inchSwapData } from '../../helpers/1inch' + +export function itBehavesLikeOneInchV5Connector( + CHAIN: number, + USDC: string, + WETH: string, + WBTC: string, + WHALE: string, + SLIPPAGE: number, + CHAINLINK_USDC_ETH: string, + CHAINLINK_WBTC_ETH: string +): void { + let weth: Contract, usdc: Contract, wbtc: Contract, whale: SignerWithAddress, priceOracle: Contract + + before('load tokens and accounts', async function () { + weth = await instanceAt('IERC20Metadata', WETH) + wbtc = await instanceAt('IERC20Metadata', WBTC) + usdc = await instanceAt('IERC20Metadata', USDC) + whale = await impersonate(WHALE, fp(100)) + }) + + before('create price oracle', async function () { + priceOracle = await deployProxy( + '@mimic-fi/v3-price-oracle/artifacts/contracts/PriceOracle.sol/PriceOracle', + [], + [ + ZERO_ADDRESS, + ZERO_ADDRESS, + WETH, + [ + { base: USDC, quote: WETH, feed: CHAINLINK_USDC_ETH }, + { base: WBTC, quote: WETH, feed: CHAINLINK_WBTC_ETH }, + ], + ] + ) + }) + + const getExpectedMinAmountOut = async ( + tokenIn: string, + tokenOut: string, + amountIn: BigNumber, + slippage: number + ): Promise => { + const price = await priceOracle['getPrice(address,address)'](tokenIn, tokenOut) + const expectedAmountOut = price.mul(amountIn).div(fp(1)) + return expectedAmountOut.sub(pct(expectedAmountOut, slippage)) + } + + context('USDC-WETH', () => { + const amountIn = toUSDC(10e3) + + it('swaps correctly USDC-WETH', async function () { + const previousBalance = await weth.balanceOf(this.connector.address) + await usdc.connect(whale).transfer(this.connector.address, amountIn) + + const data = await loadOrGet1inchSwapData(CHAIN, this.connector, usdc, weth, amountIn, SLIPPAGE) + await this.connector.connect(whale).execute(USDC, WETH, amountIn, 0, data) + + const currentBalance = await weth.balanceOf(this.connector.address) + const expectedMinAmountOut = await getExpectedMinAmountOut(USDC, WETH, amountIn, SLIPPAGE) + expect(currentBalance.sub(previousBalance)).to.be.at.least(expectedMinAmountOut) + }) + }) + + context('WETH-USDC', () => { + const amountIn = fp(1) + + it('swaps correctly WETH-USDC', async function () { + const previousBalance = await usdc.balanceOf(this.connector.address) + await weth.connect(whale).transfer(this.connector.address, amountIn) + + const data = await loadOrGet1inchSwapData(CHAIN, this.connector, weth, usdc, amountIn, SLIPPAGE) + await this.connector.connect(whale).execute(WETH, USDC, amountIn, 0, data) + + const currentBalance = await usdc.balanceOf(this.connector.address) + const expectedMinAmountOut = await getExpectedMinAmountOut(WETH, USDC, amountIn, SLIPPAGE) + expect(currentBalance.sub(previousBalance)).to.be.at.least(expectedMinAmountOut) + }) + }) + + if (WBTC !== ZERO_ADDRESS) { + context('USDC-WBTC', () => { + const amountIn = toUSDC(10e3) + + it('swaps correctly USDC-WBTC', async function () { + const previousBalance = await wbtc.balanceOf(this.connector.address) + await usdc.connect(whale).transfer(this.connector.address, amountIn) + + const data = await loadOrGet1inchSwapData(CHAIN, this.connector, usdc, wbtc, amountIn, SLIPPAGE) + await this.connector.connect(whale).execute(USDC, WBTC, amountIn, 0, data) + + const currentBalance = await wbtc.balanceOf(this.connector.address) + const expectedMinAmountOut = await getExpectedMinAmountOut(USDC, WBTC, amountIn, SLIPPAGE) + expect(currentBalance.sub(previousBalance)).to.be.at.least(expectedMinAmountOut) + }) + }) + + context('WBTC-USDC', () => { + const amountIn = toWBTC(1) + + it('swaps correctly WTBC-USDC', async function () { + const previousBalance = await usdc.balanceOf(this.connector.address) + await wbtc.connect(whale).transfer(this.connector.address, amountIn) + + const data = await loadOrGet1inchSwapData(CHAIN, this.connector, wbtc, usdc, amountIn, SLIPPAGE) + await this.connector.connect(whale).execute(WBTC, USDC, amountIn, 0, data) + + const currentBalance = await usdc.balanceOf(this.connector.address) + const expectedMinAmountOut = await getExpectedMinAmountOut(WBTC, USDC, amountIn, SLIPPAGE) + expect(currentBalance.sub(previousBalance)).to.be.at.least(expectedMinAmountOut) + }) + }) + } +} diff --git a/packages/connectors/test/swap/1inch/OneInchV5Connector.mainnet.ts b/packages/connectors/test/swap/1inch/OneInchV5Connector.mainnet.ts new file mode 100644 index 00000000..9f62c859 --- /dev/null +++ b/packages/connectors/test/swap/1inch/OneInchV5Connector.mainnet.ts @@ -0,0 +1,27 @@ +import { deploy } from '@mimic-fi/v3-helpers' + +import { itBehavesLikeOneInchV5Connector } from './OneInchV5Connector.behavior' + +/* eslint-disable no-secrets/no-secrets */ + +const CHAIN = 1 + +const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' +const WETH = '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' +const WBTC = '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599' +const WHALE = '0xf584f8728b874a6a5c7a8d4d387c9aae9172d621' + +const ONE_INCH_V5_ROUTER = '0x1111111254EEB25477B68fb85Ed929f73A960582' + +const CHAINLINK_USDC_ETH = '0x986b5E1e1755e3C2440e960477f25201B0a8bbD4' +const CHAINLINK_WBTC_ETH = '0xdeb288F737066589598e9214E782fa5A8eD689e8' + +describe('OneInchV5Connector', () => { + const SLIPPAGE = 0.015 + + before('create 1inch v5 connector', async function () { + this.connector = await deploy('OneInchV5Connector', [ONE_INCH_V5_ROUTER]) + }) + + itBehavesLikeOneInchV5Connector(CHAIN, USDC, WETH, WBTC, WHALE, SLIPPAGE, CHAINLINK_USDC_ETH, CHAINLINK_WBTC_ETH) +}) diff --git a/packages/connectors/test/swap/1inch/OneInchV5Connector.polygon.ts b/packages/connectors/test/swap/1inch/OneInchV5Connector.polygon.ts new file mode 100644 index 00000000..6359b2c9 --- /dev/null +++ b/packages/connectors/test/swap/1inch/OneInchV5Connector.polygon.ts @@ -0,0 +1,27 @@ +import { deploy } from '@mimic-fi/v3-helpers' + +import { itBehavesLikeOneInchV5Connector } from './OneInchV5Connector.behavior' + +/* eslint-disable no-secrets/no-secrets */ + +const CHAIN = 137 + +const USDC = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174' +const WETH = '0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619' +const WBTC = '0x1bfd67037b42cf73acf2047067bd4f2c47d9bfd6' +const WHALE = '0x21cb017b40abe17b6dfb9ba64a3ab0f24a7e60ea' + +const ONE_INCH_V5_ROUTER = '0x1111111254EEB25477B68fb85Ed929f73A960582' + +const CHAINLINK_USDC_ETH = '0xefb7e6be8356ccc6827799b6a7348ee674a80eae' +const CHAINLINK_WBTC_ETH = '0x19b0F0833C78c0848109E3842D34d2fDF2cA69BA' + +describe('OneInchV5Connector', () => { + const SLIPPAGE = 0.01 + + before('create 1inch v5 connector', async function () { + this.connector = await deploy('OneInchV5Connector', [ONE_INCH_V5_ROUTER]) + }) + + itBehavesLikeOneInchV5Connector(CHAIN, USDC, WETH, WBTC, WHALE, SLIPPAGE, CHAINLINK_USDC_ETH, CHAINLINK_WBTC_ETH) +}) diff --git a/packages/connectors/tsconfig.json b/packages/connectors/tsconfig.json new file mode 100644 index 00000000..03bbccc0 --- /dev/null +++ b/packages/connectors/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2019", + "module": "commonjs", + "rootDir": ".", + "outDir": "dist", + "esModuleInterop": true, + "strict": true + }, + "files": [ + "hardhat.config.ts" + ] +} diff --git a/yarn.lock b/yarn.lock index 4e142e76..cbbd0431 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2008,6 +2008,15 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3" integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg== +axios@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f" + integrity sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" @@ -4867,7 +4876,7 @@ flow-stoplight@^1.0.0: resolved "https://registry.yarnpkg.com/flow-stoplight/-/flow-stoplight-1.0.0.tgz#4a292c5bcff8b39fa6cc0cb1a853d86f27eeff7b" integrity sha512-rDjbZUKpN8OYhB0IE/vY/I8UWO/602IIJEU/76Tv4LvYnwHCk0BCsvz4eRr9n+FQcri7L5cyaXOo0+/Kh4HisA== -follow-redirects@^1.12.1: +follow-redirects@^1.12.1, follow-redirects@^1.15.0: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== @@ -4898,6 +4907,15 @@ form-data@^3.0.0: combined-stream "^1.0.8" mime-types "^2.1.12" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -7594,6 +7612,11 @@ proxy-addr@~2.0.7: forwarded "0.2.0" ipaddr.js "1.9.1" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" From f61b50693ab29449b1c70683e68a89615a92835c Mon Sep 17 00:00:00 2001 From: Facu Spagnuolo Date: Wed, 21 Jun 2023 00:31:33 -0300 Subject: [PATCH 4/7] chore: add connectors github workflow --- .github/workflows/ci-connectors.yml | 53 +++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/ci-connectors.yml diff --git a/.github/workflows/ci-connectors.yml b/.github/workflows/ci-connectors.yml new file mode 100644 index 00000000..36ff3411 --- /dev/null +++ b/.github/workflows/ci-connectors.yml @@ -0,0 +1,53 @@ +name: Connectors CI + +env: + CI: true + +on: + push: + branches: "*" + paths: + - packages/connectors/** + pull_request: + branches: "*" + paths: + - packages/connectors/** + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up environment + uses: ./.github/actions/setup + - name: Lint + run: yarn workspace @mimic-fi/v3-connectors lint + + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up environment + uses: ./.github/actions/setup + - name: Build + run: yarn build + - name: Test + run: yarn workspace @mimic-fi/v3-connectors test + + integration: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up environment + uses: ./.github/actions/setup + - name: Set up hardhat config + run: .github/scripts/setup-hardhat-config.sh ${{secrets.GOERLI_RPC}} ${{secrets.MUMBAI_RPC}} ${{secrets.MAINNET_RPC}} ${{secrets.POLYGON_RPC}} ${{secrets.OPTIMISM_RPC}} ${{secrets.ARBITRUM_RPC}} ${{secrets.GNOSIS_RPC}} ${{secrets.AVALANCHE_RPC}} ${{secrets.BSC_RPC}} ${{secrets.FANTOM_RPC}} + - name: Build + run: yarn build + - name: Test mainnet + run: yarn workspace @mimic-fi/v3-connectors test:mainnet + - name: Test polygon + run: yarn workspace @mimic-fi/v3-connectors test:polygon From f6c62a70499bd866011a64361648d9ce9db908bc Mon Sep 17 00:00:00 2001 From: Facu Spagnuolo Date: Wed, 21 Jun 2023 09:56:57 -0300 Subject: [PATCH 5/7] connectors: fix 1inch connector error message --- packages/connectors/contracts/swap/1inch/OneInchV5Connector.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/connectors/contracts/swap/1inch/OneInchV5Connector.sol b/packages/connectors/contracts/swap/1inch/OneInchV5Connector.sol index c8175a30..0a92c084 100644 --- a/packages/connectors/contracts/swap/1inch/OneInchV5Connector.sol +++ b/packages/connectors/contracts/swap/1inch/OneInchV5Connector.sol @@ -50,7 +50,7 @@ contract OneInchV5Connector { external returns (uint256 amountOut) { - require(tokenIn != tokenOut, '1INCH_SWAP_SAME_TOKEN'); + require(tokenIn != tokenOut, '1INCH_V5_SWAP_SAME_TOKEN'); uint256 preBalanceIn = IERC20(tokenIn).balanceOf(address(this)); uint256 preBalanceOut = IERC20(tokenOut).balanceOf(address(this)); From 1c90cd2aabc852399d468fd2b2cd76539a8d8af2 Mon Sep 17 00:00:00 2001 From: lgalende Date: Thu, 22 Jun 2023 03:04:14 -0300 Subject: [PATCH 6/7] connectors: implement wormhole bridge connector --- .../contracts/bridge/wormhole/IWormhole.sol | 27 +++++ .../bridge/wormhole/WormholeConnector.sol | 106 ++++++++++++++++++ packages/connectors/package.json | 5 +- .../wormhole/WormholeConnector.avalanche.ts | 22 ++++ .../wormhole/WormholeConnector.behavior.ts | 93 +++++++++++++++ .../wormhole/WormholeConnector.mainnet.ts | 22 ++++ 6 files changed, 273 insertions(+), 2 deletions(-) create mode 100644 packages/connectors/contracts/bridge/wormhole/IWormhole.sol create mode 100644 packages/connectors/contracts/bridge/wormhole/WormholeConnector.sol create mode 100644 packages/connectors/test/bridge/wormhole/WormholeConnector.avalanche.ts create mode 100644 packages/connectors/test/bridge/wormhole/WormholeConnector.behavior.ts create mode 100644 packages/connectors/test/bridge/wormhole/WormholeConnector.mainnet.ts diff --git a/packages/connectors/contracts/bridge/wormhole/IWormhole.sol b/packages/connectors/contracts/bridge/wormhole/IWormhole.sol new file mode 100644 index 00000000..9cec0b3a --- /dev/null +++ b/packages/connectors/contracts/bridge/wormhole/IWormhole.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity >=0.8.0; + +interface IWormhole { + function transferTokensWithRelay( + address token, + uint256 amount, + uint256 toNativeTokenAmount, + uint16 targetChain, + bytes32 targetRecipientWallet + ) external payable returns (uint64 messageSequence); + + function relayerFee(uint16 chainId, address token) external view returns (uint256); +} diff --git a/packages/connectors/contracts/bridge/wormhole/WormholeConnector.sol b/packages/connectors/contracts/bridge/wormhole/WormholeConnector.sol new file mode 100644 index 00000000..324ae539 --- /dev/null +++ b/packages/connectors/contracts/bridge/wormhole/WormholeConnector.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pragma solidity ^0.8.0; + +import '@openzeppelin/contracts/token/ERC20/IERC20.sol'; +import '@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol'; + +import '@mimic-fi/v3-helpers/contracts/utils/ERC20Helpers.sol'; + +import './IWormhole.sol'; + +/** + * @title WormholeConnector + * @dev Interfaces with Wormhole to bridge tokens through CCTP + */ +contract WormholeConnector { + // List of Wormhole network IDs + uint16 private constant ETHEREUM_WORMHOLE_NETWORK_ID = 2; + uint16 private constant POLYGON_WORMHOLE_NETWORK_ID = 5; + uint16 private constant ARBITRUM_WORMHOLE_NETWORK_ID = 23; + uint16 private constant OPTIMISM_WORMHOLE_NETWORK_ID = 24; + uint16 private constant BSC_WORMHOLE_NETWORK_ID = 4; + uint16 private constant FANTOM_WORMHOLE_NETWORK_ID = 10; + uint16 private constant AVALANCHE_WORMHOLE_NETWORK_ID = 6; + + // List of chain IDs supported by Wormhole + uint256 private constant ETHEREUM_ID = 1; + uint256 private constant POLYGON_ID = 137; + uint256 private constant ARBITRUM_ID = 42161; + uint256 private constant OPTIMISM_ID = 10; + uint256 private constant BSC_ID = 56; + uint256 private constant FANTOM_ID = 250; + uint256 private constant AVALANCHE_ID = 43114; + + // Reference to the Wormhole's CircleRelayer contract of the source chain + IWormhole private immutable wormholeCircleRelayer; + + /** + * @dev Creates a new Wormhole connector + * @param _wormholeCircleRelayer Address of the Wormhole's CircleRelayer contract for the source chain + */ + constructor(address _wormholeCircleRelayer) { + wormholeCircleRelayer = IWormhole(_wormholeCircleRelayer); + } + + /** + * @dev Executes a bridge of assets using Wormhole's CircleRelayer integration + * @param chainId ID of the destination chain + * @param token Address of the token to be bridged + * @param amountIn Amount of tokens to be bridged + * @param minAmountOut Minimum amount of tokens willing to receive on the destination chain + * @param recipient Address that will receive the tokens on the destination chain + */ + function execute(uint256 chainId, address token, uint256 amountIn, uint256 minAmountOut, address recipient) + external + { + require(block.chainid != chainId, 'WORMHOLE_BRIDGE_SAME_CHAIN'); + require(recipient != address(0), 'WORMHOLE_BRIDGE_RECIPIENT_ZERO'); + + uint16 wormholeNetworkId = _getWormholeNetworkId(chainId); + uint256 relayerFee = wormholeCircleRelayer.relayerFee(wormholeNetworkId, token); + require(minAmountOut <= amountIn - relayerFee, 'WORMHOLE_MIN_AMOUNT_OUT_TOO_BIG'); + + uint256 preBalanceIn = IERC20(token).balanceOf(address(this)); + + ERC20Helpers.approve(token, address(wormholeCircleRelayer), amountIn); + wormholeCircleRelayer.transferTokensWithRelay( + token, + amountIn, + 0, // don't swap to native token + wormholeNetworkId, + bytes32(uint256(uint160(recipient))) // convert from address to bytes32 + ); + + uint256 postBalanceIn = IERC20(token).balanceOf(address(this)); + require(postBalanceIn >= preBalanceIn - amountIn, 'WORMHOLE_BAD_TOKEN_IN_BALANCE'); + } + + /** + * @dev Internal function to tell the Wormhole network ID based on a chain ID + * @param chainId ID of the chain being queried + * @return Wormhole network ID associated to the requested chain ID + */ + function _getWormholeNetworkId(uint256 chainId) private pure returns (uint16) { + if (chainId == ETHEREUM_ID) return ETHEREUM_WORMHOLE_NETWORK_ID; + else if (chainId == POLYGON_ID) return POLYGON_WORMHOLE_NETWORK_ID; + else if (chainId == ARBITRUM_ID) return ARBITRUM_WORMHOLE_NETWORK_ID; + else if (chainId == OPTIMISM_ID) return OPTIMISM_WORMHOLE_NETWORK_ID; + else if (chainId == BSC_ID) return BSC_WORMHOLE_NETWORK_ID; + else if (chainId == FANTOM_ID) return FANTOM_WORMHOLE_NETWORK_ID; + else if (chainId == AVALANCHE_ID) return AVALANCHE_WORMHOLE_NETWORK_ID; + else revert('WORMHOLE_UNKNOWN_CHAIN_ID'); + } +} diff --git a/packages/connectors/package.json b/packages/connectors/package.json index 78b77707..ea8d4319 100644 --- a/packages/connectors/package.json +++ b/packages/connectors/package.json @@ -15,8 +15,9 @@ "lint:solidity": "solhint 'contracts/**/*.sol' --config ../../node_modules/solhint-config-mimic/index.js", "lint:typescript": "eslint . --ext .ts", "test": "hardhat test", - "test:mainnet": "yarn test --fork mainnet --block-number 17525323", - "test:polygon": "yarn test --fork polygon --block-number 44153231", + "test:mainnet": "yarn test --fork mainnet --block-number 17525323 --chain-id 1", + "test:polygon": "yarn test --fork polygon --block-number 44153231 --chain-id 137", + "test:avalanche": "yarn test --fork avalanche --block-number 31333905 --chain-id 43114", "prepare": "yarn build" }, "dependencies": { diff --git a/packages/connectors/test/bridge/wormhole/WormholeConnector.avalanche.ts b/packages/connectors/test/bridge/wormhole/WormholeConnector.avalanche.ts new file mode 100644 index 00000000..f1d39991 --- /dev/null +++ b/packages/connectors/test/bridge/wormhole/WormholeConnector.avalanche.ts @@ -0,0 +1,22 @@ +import { deploy } from '@mimic-fi/v3-helpers' + +import { itBehavesLikeWormholeConnector } from './WormholeConnector.behavior' + +/* eslint-disable no-secrets/no-secrets */ + +const USDC = '0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E' +const WHALE = '0xbbff2a8ec8d702e61faaccf7cf705968bb6a5bab' + +const WORMHOLE_CIRCLE_RELAYER = '0x32DeC3F4A0723Ce02232f87e8772024E0C86d834' + +describe('WormholeConnector', () => { + const SOURCE_CHAIN_ID = 43114 + + before('create bridge connector', async function () { + this.connector = await deploy('WormholeConnector', [WORMHOLE_CIRCLE_RELAYER]) + }) + + context('USDC', () => { + itBehavesLikeWormholeConnector(SOURCE_CHAIN_ID, USDC, WHALE) + }) +}) diff --git a/packages/connectors/test/bridge/wormhole/WormholeConnector.behavior.ts b/packages/connectors/test/bridge/wormhole/WormholeConnector.behavior.ts new file mode 100644 index 00000000..074ef38a --- /dev/null +++ b/packages/connectors/test/bridge/wormhole/WormholeConnector.behavior.ts @@ -0,0 +1,93 @@ +import { bn, fp, impersonate, instanceAt, ZERO_ADDRESS } from '@mimic-fi/v3-helpers' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' +import { expect } from 'chai' +import { BigNumber, Contract } from 'ethers' + +export function itBehavesLikeWormholeConnector( + sourceChainId: number, + tokenAddress: string, + whaleAddress: string +): void { + let token: Contract, whale: SignerWithAddress + + before('load tokens and accounts', async function () { + token = await instanceAt('IERC20Metadata', tokenAddress) + whale = await impersonate(whaleAddress, fp(100)) + }) + + context('when the recipient is not the zero address', async () => { + let amountIn: BigNumber + let minAmountOut: BigNumber + + const relayerFee = bn(35000000) + + beforeEach('set amount in and min amount out', async () => { + const decimals = await token.decimals() + amountIn = bn(300).mul(bn(10).pow(decimals)) + minAmountOut = amountIn.sub(relayerFee) + }) + + function bridgesProperly(destinationChainId: number) { + if (destinationChainId != sourceChainId) { + it('should send the tokens to the gateway', async function () { + const previousSenderBalance = await token.balanceOf(whale.address) + const previousTotalSupply = await token.totalSupply() + const previousConnectorBalance = await token.balanceOf(this.connector.address) + + await token.connect(whale).transfer(this.connector.address, amountIn) + await this.connector + .connect(whale) + .execute(destinationChainId, tokenAddress, amountIn, minAmountOut, whale.address) + + const currentSenderBalance = await token.balanceOf(whale.address) + expect(currentSenderBalance).to.be.equal(previousSenderBalance.sub(amountIn)) + + // check tokens are burnt on the source chain + const currentTotalSupply = await token.totalSupply() + expect(currentTotalSupply).to.be.equal(previousTotalSupply.sub(amountIn)) + + const currentConnectorBalance = await token.balanceOf(this.connector.address) + expect(currentConnectorBalance).to.be.equal(previousConnectorBalance) + }) + } else { + it('reverts', async function () { + await expect( + this.connector + .connect(whale) + .execute(destinationChainId, tokenAddress, amountIn, minAmountOut, whale.address) + ).to.be.revertedWith('WORMHOLE_BRIDGE_SAME_CHAIN') + }) + } + } + + context('bridge to avalanche', () => { + const destinationChainId = 43114 + + bridgesProperly(destinationChainId) + }) + + context('bridge to mainnet', () => { + const destinationChainId = 1 + + bridgesProperly(destinationChainId) + }) + + context('bridge to goerli', () => { + const destinationChainId = 5 + + it('reverts', async function () { + await expect( + this.connector.connect(whale).execute(destinationChainId, tokenAddress, amountIn, minAmountOut, whale.address) + ).to.be.revertedWith('WORMHOLE_UNKNOWN_CHAIN_ID') + }) + }) + }) + + context('when the recipient is the zero address', async () => { + it('reverts', async function () { + await expect(this.connector.connect(whale).execute(0, tokenAddress, 0, 0, ZERO_ADDRESS)).to.be.revertedWith( + 'WORMHOLE_BRIDGE_RECIPIENT_ZERO' + ) + }) + }) +} diff --git a/packages/connectors/test/bridge/wormhole/WormholeConnector.mainnet.ts b/packages/connectors/test/bridge/wormhole/WormholeConnector.mainnet.ts new file mode 100644 index 00000000..7a0a1103 --- /dev/null +++ b/packages/connectors/test/bridge/wormhole/WormholeConnector.mainnet.ts @@ -0,0 +1,22 @@ +import { deploy } from '@mimic-fi/v3-helpers' + +import { itBehavesLikeWormholeConnector } from './WormholeConnector.behavior' + +/* eslint-disable no-secrets/no-secrets */ + +const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' +const WHALE = '0xf584f8728b874a6a5c7a8d4d387c9aae9172d621' + +const WORMHOLE_CIRCLE_RELAYER = '0x32DeC3F4A0723Ce02232f87e8772024E0C86d834' + +describe('WormholeConnector', () => { + const SOURCE_CHAIN_ID = 1 + + before('create wormhole connector', async function () { + this.connector = await deploy('WormholeConnector', [WORMHOLE_CIRCLE_RELAYER]) + }) + + context('USDC', () => { + itBehavesLikeWormholeConnector(SOURCE_CHAIN_ID, USDC, WHALE) + }) +}) From 0d510d284660ee69f88abb8b28deea9e2d0d749a Mon Sep 17 00:00:00 2001 From: lgalende Date: Thu, 22 Jun 2023 11:39:24 -0300 Subject: [PATCH 7/7] connectors: wormhole visibility and ci file --- .github/workflows/ci-connectors.yml | 2 ++ .../contracts/bridge/wormhole/WormholeConnector.sol | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-connectors.yml b/.github/workflows/ci-connectors.yml index 36ff3411..cdf1632a 100644 --- a/.github/workflows/ci-connectors.yml +++ b/.github/workflows/ci-connectors.yml @@ -51,3 +51,5 @@ jobs: run: yarn workspace @mimic-fi/v3-connectors test:mainnet - name: Test polygon run: yarn workspace @mimic-fi/v3-connectors test:polygon + - name: Test avalanche + run: yarn workspace @mimic-fi/v3-connectors test:avalanche diff --git a/packages/connectors/contracts/bridge/wormhole/WormholeConnector.sol b/packages/connectors/contracts/bridge/wormhole/WormholeConnector.sol index 324ae539..24ba0dfc 100644 --- a/packages/connectors/contracts/bridge/wormhole/WormholeConnector.sol +++ b/packages/connectors/contracts/bridge/wormhole/WormholeConnector.sol @@ -45,7 +45,7 @@ contract WormholeConnector { uint256 private constant AVALANCHE_ID = 43114; // Reference to the Wormhole's CircleRelayer contract of the source chain - IWormhole private immutable wormholeCircleRelayer; + IWormhole public immutable wormholeCircleRelayer; /** * @dev Creates a new Wormhole connector @@ -89,11 +89,11 @@ contract WormholeConnector { } /** - * @dev Internal function to tell the Wormhole network ID based on a chain ID + * @dev Tells the Wormhole network ID based on a chain ID * @param chainId ID of the chain being queried * @return Wormhole network ID associated to the requested chain ID */ - function _getWormholeNetworkId(uint256 chainId) private pure returns (uint16) { + function _getWormholeNetworkId(uint256 chainId) internal pure returns (uint16) { if (chainId == ETHEREUM_ID) return ETHEREUM_WORMHOLE_NETWORK_ID; else if (chainId == POLYGON_ID) return POLYGON_WORMHOLE_NETWORK_ID; else if (chainId == ARBITRUM_ID) return ARBITRUM_WORMHOLE_NETWORK_ID;