Skip to content

Commit

Permalink
Connectors: Implement wormhole bridge connector (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
lgalende authored Jun 26, 2023
1 parent edcbf48 commit 75ca506
Show file tree
Hide file tree
Showing 9 changed files with 284 additions and 11 deletions.
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)
})
})

0 comments on commit 75ca506

Please sign in to comment.