Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Connectors: Implement wormhole bridge connector #15

Merged
merged 10 commits into from
Jun 26, 2023
Merged
10 changes: 5 additions & 5 deletions .github/scripts/setup-hardhat-config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
2 changes: 2 additions & 0 deletions .github/workflows/ci-connectors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion .github/workflows/ci-price-oracle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions packages/connectors/contracts/bridge/wormhole/IWormhole.sol
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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);
}
106 changes: 106 additions & 0 deletions packages/connectors/contracts/bridge/wormhole/WormholeConnector.sol
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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');
}
}
11 changes: 6 additions & 5 deletions packages/connectors/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
})
})
Original file line number Diff line number Diff line change
@@ -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'
)
})
})
}
Original file line number Diff line number Diff line change
@@ -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)
})
})