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)
+ })
+})