diff --git a/.github/scripts/setup-hardhat-config.sh b/.github/scripts/setup-hardhat-config.sh index 2ad588e4..29c65186 100755 --- a/.github/scripts/setup-hardhat-config.sh +++ b/.github/scripts/setup-hardhat-config.sh @@ -4,9 +4,9 @@ POLYGON_URL="$2" OPTIMISM_URL="$3" ARBITRUM_URL="$4" GNOSIS_URL="$5" -BSC_URL="$6" -FANTOM_URL="$7" -AVALANCHE_URL="$8" +AVALANCHE_URL="$6" +BSC_URL="$7" +FANTOM_URL="$8" set -o errexit @@ -20,9 +20,9 @@ echo " \"optimism\": { \"url\": \"${OPTIMISM_URL}\" }, \"arbitrum\": { \"url\": \"${ARBITRUM_URL}\" }, \"gnosis\": { \"url\": \"${GNOSIS_URL}\" }, + \"avalanche\": { \"url\": \"${AVALANCHE_URL}\" }, \"bsc\": { \"url\": \"${BSC_URL}\" }, - \"fantom\": { \"url\": \"${FANTOM_URL}\" }, - \"avalanche\": { \"url\": \"${AVALANCHE_URL}\" } + \"fantom\": { \"url\": \"${FANTOM_URL}\" } } } " > $HOME/.hardhat/networks.mimic.json diff --git a/.github/workflows/ci-connectors.yml b/.github/workflows/ci-connectors.yml index 3e5ff865..92d07c16 100644 --- a/.github/workflows/ci-connectors.yml +++ b/.github/workflows/ci-connectors.yml @@ -57,3 +57,5 @@ jobs: run: yarn workspace @mimic-fi/v3-connectors test:arbitrum - name: Test gnosis run: yarn workspace @mimic-fi/v3-connectors test:gnosis + - name: Test avalanche + run: yarn workspace @mimic-fi/v3-connectors test:avalanche diff --git a/.github/workflows/ci-price-oracle.yml b/.github/workflows/ci-price-oracle.yml index f38677c8..9cc81b11 100644 --- a/.github/workflows/ci-price-oracle.yml +++ b/.github/workflows/ci-price-oracle.yml @@ -44,7 +44,7 @@ jobs: - 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}} + run: .github/scripts/setup-hardhat-config.sh ${{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 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..24ba0dfc --- /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 public 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 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) 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; + 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 6275913f..7b720bc4 100644 --- a/packages/connectors/package.json +++ b/packages/connectors/package.json @@ -15,11 +15,12 @@ "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:optimism": "yarn test --fork optimism --block-number 105914596", - "test:arbitrum": "yarn test --fork arbitrum --block-number 105116582", - "test:gnosis": "yarn test --fork gnosis --block-number 28580764", + "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:optimism": "yarn test --fork optimism --block-number 105914596 --chain-id 10", + "test:arbitrum": "yarn test --fork arbitrum --block-number 105116582 --chain-id 42161", + "test:gnosis": "yarn test --fork gnosis --block-number 28580764 --chain-id 100", + "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..eff8822e --- /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) + }) +})